Add an HTML Content Block to the Main Zone
Block Properties:
No changes are needed for the block properties.
HTML Content:
<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;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cdefs%3E%3Cpattern id='transparent-grid' x='0' y='0' width='16' height='16' patternUnits='userSpaceOnUse'%3E%3Crect width='8' height='8' fill='%23d2d2d2' /%3E%3Crect x='8' y='8' width='8' height='8' fill='%23d2d2d2' /%3E%3C/pattern%3E%3C/defs%3E%3Crect width='100%25' height='100%25' fill='url(%23transparent-grid)' /%3E%3C/svg%3E");
}
#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>