1 CSS Color Variables Quick Reference Shared by Jeff Richmond, The Well Community Church 2 days ago Web, CMS Intermediate Description I'm in the process of getting our Rock instance ready to upgrade to v18, which mostly consists of finding the many places where my internal site customizations are broken in the new RockNextGen theme… especially in dark mode. During this process I found myself repeatedly having to sift through the theme's CSS to find various color variables that I needed for my fixes. In a fit of frustration (and procrastination), I came up with this recipe that finds all color variables available to the page and displays them for quick reference. Each variable is displayed with a color swatch, the variable name, the variable's raw value and, when applicable, the final calculated color value formatted as a hex value, followed by an opacity percentage. Clicking on the variable name will copy the name, wrapped with var() to your clipboard for easy pasting into your own CSS. How-To Upload the Transparent Background Image Upload the check-pattern.png file to your server. I uploaded mine to content/internalsite/images/check-pattern.png. You can upload it wherever you want. Just be sure to make a note of its location for later. Create a "CSS Color Variables" Page We have a "Rock Dev" section under out Admin Tools menu, so I placed the new page there. You can put it wherever makes the most sense for you. Page Title: CSS Color Variables Site: Rock RMS Layout: Full Width Icon CSS Class: fa fa-palette Page Routes: admin/dev/css-colors Add an HTML Content Block to the Main Zone Block Properties: No changes are needed for the block properties. HTML Content: Double check the swatch background image path in the CSS near the bottom to make sure it matches where you uploaded check-pattern.png. <div id="CSSColorSwatches"></div> <script> /**** General Helper Utilities ****************************************/ // Single shared test element for color validation and computing var globalTestElement = null; // Return a reusable off-screen element for browser-based color testing function getTestElement() { // Create the element the first time it is needed if (!globalTestElement) { globalTestElement = document.createElement('span'); // Position it off-screen so it does not affect page layout // or appear visually to the user. globalTestElement.style.position = 'absolute'; globalTestElement.style.left = '-9999px'; globalTestElement.style.top = '-9999px'; document.body.appendChild(globalTestElement); } // Always reset the color value globalTestElement.style.color = ''; return globalTestElement; } // Escape text so it can be safely inserted into HTML function escapeHTML(htmlString) { if (htmlString === null || htmlString === undefined) { return ''; } return String(htmlString) .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Create a debounced version of a function to delay execution until // an event stops firing for the specified amount of time function debounce(callbackFunction, delayMilliseconds) { var timeoutId; return function() { var context = this; var args = arguments; clearTimeout(timeoutId); timeoutId = setTimeout(function() { callbackFunction.apply(context, args); }, delayMilliseconds); }; } /**** Color Parsing Helpers ****************************************/ // Ensure a numeric RGB channel stays within the 0-255 range function clampColorChannel(channelValue) { // Return null if non-numeric if (isNaN(channelValue)) { return null; } return Math.max(0, Math.min(255, channelValue)); } // Ensure an alpha value stays within the 0-1 range function clampAlpha(alphaValue) { // Return null if non-numeric if (isNaN(alphaValue)) { return null; } return Math.max(0, Math.min(1, alphaValue)); } // Convert a single RGB component to a hex string function componentToHex(colorComponent) { var hexValue = colorComponent.toString(16); return hexValue.length === 1 ? '0' + hexValue : hexValue; } // Parse a single RGB channel value (percentage or whole numbers) function parseRGBChannel(channelValue) { if (channelValue === undefined || channelValue === null) { return null; } // Convert percentages to whole numbers if (/%$/.test(channelValue)) { return clampColorChannel(Math.round(parseFloat(channelValue) * 255 / 100)); } return clampColorChannel(Math.round(parseFloat(channelValue))); } // Parse a single alpha channel value (decimal, percentage or null) function parseAlphaChannel(alphaValue) { // Treat missing or null values as 100% opaque if (alphaValue === undefined || alphaValue === null || alphaValue === '') { return 1; } // Convert percentages to whole numbers if (/%$/.test(alphaValue)) { return clampAlpha(parseFloat(alphaValue) / 100); } return clampAlpha(parseFloat(alphaValue)); } // Build a normalized color object after all channels have been parsed function buildParsedColor(colorParts) { if ( colorParts.red === null || colorParts.green === null || colorParts.blue === null || colorParts.alpha === null ) { return null; } return { hex: '#' + componentToHex(colorParts.red) + componentToHex(colorParts.green) + componentToHex(colorParts.blue), alpha: colorParts.alpha }; } // Parse rgb() and rgba() color strings function parseRGBColor(colorString) { // Match classic comma-separated rgb()/rgba() syntax var rgbCommaMatch = colorString.match(/^rgba?\(\s*([\d.]+%?)\s*,\s*([\d.]+%?)\s*,\s*([\d.]+%?)(?:\s*,\s*([\d.]+%?))?\s*\)$/i); if (rgbCommaMatch) { return buildParsedColor( { red: parseRGBChannel(rgbCommaMatch[1]), green: parseRGBChannel(rgbCommaMatch[2]), blue: parseRGBChannel(rgbCommaMatch[3]), alpha: parseAlphaChannel(rgbCommaMatch[4]) }); } // Match newer space-separated rgb()/rgba() syntax var rgbSpaceMatch = colorString.match(/^rgba?\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+%?)(?:\s*\/\s*([\d.]+%?))?\s*\)$/i); if (rgbSpaceMatch) { return buildParsedColor( { red: parseRGBChannel(rgbSpaceMatch[1]), green: parseRGBChannel(rgbSpaceMatch[2]), blue: parseRGBChannel(rgbSpaceMatch[3]), alpha: parseAlphaChannel(rgbSpaceMatch[4]) }); } return null; } // Parse the CSS color() function for the srgb color space function parseColorFunction(colorString) { var colorMatch = colorString.match(/^color\(\s*srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+%?))?\s*\)$/i); if (!colorMatch) { return null; } return buildParsedColor( { red: clampColorChannel(Math.round(parseFloat(colorMatch[1]) * 255)), green: clampColorChannel(Math.round(parseFloat(colorMatch[2]) * 255)), blue: clampColorChannel(Math.round(parseFloat(colorMatch[3]) * 255)), alpha: parseAlphaChannel(colorMatch[4]) }); } // Attempt to parse any supported browser color string into a normalized object (rgb(), rgba(), or color(srgb...)) function parseAnyColorString(colorString) { if (!colorString) { return null; } // Normalize whitespace/casing before attempting regex parsing var normalizedColor = colorString.trim().toLowerCase(); return parseRGBColor(normalizedColor) || parseColorFunction(normalizedColor); } /**** Color Detection and Formatting ****************************************/ // Determine whether a string can be rendered by the browser as a valid CSS color function isRenderableColor(propertyValue) { if (!propertyValue) { return false; } // Preferred modern check if (window.CSS && CSS.supports('color', propertyValue)) { return true; } // Fallback browser acceptance check var testElement = getTestElement(); testElement.style.color = propertyValue; return testElement.style.color !== ''; } // determines whether the raw CSS variable value should also display a normalized computed value line function shouldShowComputedValue(propertyValue) { if (!propertyValue) { return false; } // Detect color expressions that are formula-based and benefit from a computed display value return /\b(color-mix|var|rgb|rgba)\s*\(/i.test(propertyValue); } // Resolve a CSS color expression and return the computed color string function getComputedColorString(propertyValue) { var testElement = getTestElement(); testElement.style.color = propertyValue; return window.getComputedStyle(testElement).color; } // Convert a renderable CSS color into a normalized display format // Colors are displayed as a 6-digit hex value followed by an optional opacity percentage function formatComputedDisplayValue(propertyValue) { var computedColor = getComputedColorString(propertyValue); var parsedColor = parseAnyColorString(computedColor); if (!parsedColor) { return ''; } // Convert alpha to a whole-number opacity percentage for display var opacityPercent = Math.round(parsedColor.alpha * 100); // Return semi-transparent colors in the format: #123456, 78% if (opacityPercent < 100) { return parsedColor.hex + ', ' + opacityPercent + '%'; } // Return fully opaque colors as 6-digit hex values return parsedColor.hex; } /**** Clipboard and Tooltips ****************************************/ // Copy text to the clipboard function copyTextToClipboard(textToCopy) { // Use the Clipboard API when available and the page is running in a secure context if (navigator.clipboard && window.isSecureContext) { return navigator.clipboard.writeText(textToCopy); } // Fall back to a temporary input and document.execCommand('copy') for older browsers return new Promise(function(resolve, reject) { var $tempInput = $('<input type="text" class="css-var-temp-input">'); $('body').append($tempInput); $tempInput.val(textToCopy).trigger('focus').trigger('select'); try { var copySucceeded = document.execCommand('copy'); $tempInput.remove(); if (copySucceeded) { resolve(); } else { reject(); } } catch (error) { $tempInput.remove(); reject(error); } }); } //***********************************************************************/ //**** Extract Raw CSS Variables //***********************************************************************/ // Extract raw CSS custom property values from all available stylesheets // // This is to preserve the original declared value, such as var(--some-color) // or color-mix(...) rather than only showing the browser's computed result function getRawCSSVariables() { var variables = {}; // Recursively scan a CSSRuleList and record custom properties found in matching :root rules function extractVariables(rules) { for (let rule of rules) { // Standard style rules // :root { --color-primary: #123456; } if (rule instanceof CSSStyleRule) { if (rule.selectorText.includes(':root')) { for (let property of rule.style) { if (property.startsWith('--')) { variables[property] = rule.style.getPropertyValue(property).trim(); } } } } // Media query rules // @media (prefers-color-scheme: dark) { ... } else if (rule instanceof CSSMediaRule) { // Only descend into media rules that currently match if (window.matchMedia(rule.media.mediaText).matches) { extractVariables(rule.cssRules); } } // Supports rules // @supports (color: color-mix(...)) { ... } else if (rule instanceof CSSSupportsRule) { extractVariables(rule.cssRules); } } } // Scan all accessible stylesheets on the page // Some stylesheets may be blocked by browser security rules for (let sheet of document.styleSheets) { try { extractVariables(sheet.cssRules); } catch (error) { console.warn('Could not access stylesheet', error); } } return variables; } //***********************************************************************/ //**** Main Render Function //***********************************************************************/ // Build the full swatch UI for all renderable CSS custom properties // // For each CSS variable: // 1. Get the raw declared value // 2. Get the browser-computed value // 3. Filter out anything that does not render as a color // 4. Build the final card markup function renderCSSVariableSwatches() { var computedStyles; var rawVariables = getRawCSSVariables(); var variables = []; // Use the newer CSS Typed OM API when available // This can provide computed values in a cleaner structured way if ('computedStyleMap' in document.documentElement) { computedStyles = document.documentElement.computedStyleMap(); Array.from(computedStyles).forEach(function(entry) { var propertyName = entry[0]; var propertyValue = entry[1]; if (propertyName.startsWith('--')) { var computedValue = propertyValue.toString().trim(); var rawValue = rawVariables[propertyName] || computedValue; // Only keep variables that the browser can actually render as colors if (isRenderableColor(computedValue)) { variables.push( { name: propertyName, value: rawValue, renderedValue: computedValue, computedValue: shouldShowComputedValue(rawValue) ? formatComputedDisplayValue(computedValue) : '' }); } } }); } else { // Fallback for browsers that do not support computedStyleMap() computedStyles = getComputedStyle(document.documentElement); for (var i = 0; i < computedStyles.length; i++) { var propertyName = computedStyles[i]; if (propertyName.startsWith('--')) { var computedValue = computedStyles.getPropertyValue(propertyName).trim(); var rawValue = rawVariables[propertyName] || computedValue; // Only keep variables that the browser can actually render as colors if (isRenderableColor(computedValue)) { variables.push( { name: propertyName, value: rawValue, renderedValue: computedValue, computedValue: shouldShowComputedValue(rawValue) ? formatComputedDisplayValue(computedValue) : '' }); } } } } // Sort alphabetically by variable name so the list is predictable variables.sort(function(a, b) { return a.name.localeCompare(b.name); }); var containerHTML = '<div class="css-var-swatches">'; for (var j = 0; j < variables.length; j++) { var variableItem = variables[j]; var computedValueHTML = ''; // Only include the third line if a computed value should be included if (variableItem.computedValue) { computedValueHTML = '<small class="css-var-value">' + escapeHTML(variableItem.computedValue) + '</small>'; } containerHTML += '' + '<div class="css-var-column-item">' + ' <div class="css-var-item card mb-2">' + ' <div class="card-body p-2">' + ' <div class="css-var-top">' + ' <div class="css-var-swatch"><span class="css-var-swatch-color" style="background-color: ' + escapeHTML(variableItem.renderedValue) + ';"></span></div>' + ' <div class="css-var-label">' + ' <a href="#" class="css-var-name" data-variable-name="' + escapeHTML(variableItem.name) + '" title="' + escapeHTML(variableItem.name) + '">' + escapeHTML(variableItem.name) + '</a>' + ' <small class="css-var-formula" title="' + escapeHTML(variableItem.value) + '">' + escapeHTML(variableItem.value) + '</small>' + computedValueHTML + ' </div>' + ' </div>' + ' </div>' + ' </div>' + '</div>'; } containerHTML += '</div>'; // Replace any existing swatches with the new rendered markup $('#CSSColorSwatches').empty().append(containerHTML); // Clean up the shared test element after rendering is complete if (globalTestElement && globalTestElement.parentNode) { globalTestElement.parentNode.removeChild(globalTestElement); globalTestElement = null; } } /**** Initialization **********************************************/ // Initialize the swatch UI when the page is ready $(function() { renderCSSVariableSwatches(); // Clicking a variable name copies the variable wrapped in var() for pasting directly into CSS $('#CSSColorSwatches').on('click', '.css-var-name', function(event) { event.preventDefault(); var $link = $(this); var variableName = $link.attr('data-variable-name'); var originalText = $link.text(); var copyValue = 'var(' + variableName + ')'; copyTextToClipboard(copyValue).then(function() { $link.text('Copied'); setTimeout(function() { $link.text(originalText); }, 1200); }).catch(function() { $link.text('Failed'); setTimeout(function() { $link.text(originalText); }, 1200); }); }); }); </script> <style> #CSSColorSwatches .css-var-swatches { column-gap: 15px; } #CSSColorSwatches .css-var-column-item { display: inline-block; width: 100%; margin-bottom: 0; break-inside: avoid; -webkit-column-break-inside: avoid; page-break-inside: avoid; } #CSSColorSwatches .css-var-item { display: block; } #CSSColorSwatches .css-var-top { position: relative; min-height: 50px; } #CSSColorSwatches .css-var-swatch { position: absolute; left: 0; top: 0; border: 1px solid #ccc; width: 50px; height: 50px; overflow: hidden; background-color: #fff; background-repeat: repeat; /****************************************************************************/ /*** Change this URL to match the location of your check-patter.png file ****/ /****************************************************************************/ background-image: url('/content/internalsite/images/check-pattern.png'); } #CSSColorSwatches .css-var-swatch-color { display: block; width: 100%; height: 100%; } #CSSColorSwatches .css-var-label { margin-left: 60px; min-width: 0; font-family: monospace; font-size: 14px; } #CSSColorSwatches .css-var-name { display: block; word-break: break-word; font-weight: bold; cursor: pointer; } #CSSColorSwatches .css-var-formula, #CSSColorSwatches .css-var-value { display: block; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } @media (max-width: 767px) { #CSSColorSwatches .css-var-swatches { column-count: 1; } } @media (min-width: 768px) and (max-width: 1199px) { #CSSColorSwatches .css-var-swatches { column-count: 2; } } @media (min-width: 1200px) and (max-width: 1399px) { #CSSColorSwatches .css-var-swatches { column-count: 3; } } @media (min-width: 1400px) and (max-width: 1799px) { #CSSColorSwatches .css-var-swatches { column-count: 4; } } @media (min-width: 1800px) and (max-width: 1999px) { #CSSColorSwatches .css-var-swatches { column-count: 5; } } @media (min-width: 2000px) { #CSSColorSwatches .css-var-swatches { column-count: 6; } } </style> That's it! Reload the page and you should see swatches for every color variable in the theme's CSS. Find the color you want and click its name to copy the CSS-ready var(--color-variable-name) snippet. Some things to keep in mind: The values for each variable will match the currently active color mode (light/dark) The values will not update automatically if you change your system's color mode with the page already loaded. The page will need to be reloaded to see the new values. This recipe can be used on any Rock site (actually, it should work on any non-Rock website as well). It will always display the color variables available on that page. The original raw values may not be available for variables that are located in cross-origin stylesheets. Follow Up Please don't hesitate to leave a comment below or hit me up on Rock Chat (@JeffRichmond) if you have questions or find any issues with this recipe. If you come up with better or more efficient ways of doing anything in this recipe, please let me know. Thanks! Change Log 2026-03-20 - Initial Version Download File