Description

css-color-swatches-light.jpg
css-color-swatches-dark.jpg

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

  1. 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.

  2. 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
  3. 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, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#39;');
        }
    
        // 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