UI Components Medium
Accessible Color Token System
Design token system with semantic color variables that guarantee WCAG contrast compliance across all token combinations.
Open in Lab
MCP
css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
DARK THEME TOKENS (default)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
:root {
color-scheme: dark;
/* Text */
--color-text-primary: #e5e5e5;
--color-text-secondary: #a3a3a3;
--color-text-muted: #737373;
--color-text-inverse: #0a0a0a;
/* Backgrounds */
--color-bg-base: #0a0a0a;
--color-bg-surface: #141414;
--color-bg-elevated: #1a1a1a;
--color-bg-overlay: #262626;
/* Borders */
--color-border-default: #262626;
--color-border-strong: #404040;
/* Accent */
--color-accent: #3b82f6;
--color-accent-text: #ffffff;
/* States */
--color-success: #4ade80;
--color-warning: #fbbf24;
--color-error: #f87171;
--color-info: #60a5fa;
--transition: 0.3s;
}
/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
LIGHT THEME TOKENS
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
.light {
color-scheme: light;
--color-text-primary: #111111;
--color-text-secondary: #525252;
--color-text-muted: #737373;
--color-text-inverse: #ffffff;
--color-bg-base: #ffffff;
--color-bg-surface: #f5f5f5;
--color-bg-elevated: #fafafa;
--color-bg-overlay: #e5e5e5;
--color-border-default: #e5e5e5;
--color-border-strong: #d4d4d4;
--color-accent: #2563eb;
--color-accent-text: #ffffff;
--color-success: #16a34a;
--color-warning: #d97706;
--color-error: #dc2626;
--color-info: #2563eb;
}
/* โโ Base โโ */
body {
font-family: Inter, system-ui, -apple-system, sans-serif;
background: var(--color-bg-base);
color: var(--color-text-primary);
min-height: 100vh;
display: flex;
justify-content: center;
padding: 2rem;
transition: background var(--transition), color var(--transition);
}
.page {
width: 100%;
max-width: 840px;
}
/* โโ Header โโ */
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border-default);
}
.header-title {
font-size: 1.5rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.header-sub {
color: var(--color-text-muted);
font-size: 0.8125rem;
margin-top: 0.25rem;
}
.theme-btn {
display: flex;
align-items: center;
gap: 0.375rem;
background: var(--color-bg-surface);
border: 1px solid var(--color-border-default);
border-radius: 0.5rem;
color: var(--color-text-primary);
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.theme-btn:hover {
background: var(--color-bg-elevated);
}
.icon-sun {
display: none;
}
.icon-moon {
display: block;
}
.light .icon-sun {
display: block;
}
.light .icon-moon {
display: none;
}
/* โโ Sections โโ */
.section {
margin-bottom: 2.5rem;
}
.section-title {
font-size: 1.125rem;
font-weight: 700;
margin-bottom: 0.75rem;
}
.section-desc {
color: var(--color-text-secondary);
font-size: 0.8125rem;
margin-bottom: 1rem;
line-height: 1.5;
}
/* โโ Token grid โโ */
.token-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.token-category {
background: var(--color-bg-surface);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem;
padding: 1rem;
transition: background var(--transition), border-color var(--transition);
}
.token-cat-title {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-text-muted);
margin-bottom: 0.75rem;
}
.token-row {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0;
border-bottom: 1px solid var(--color-border-default);
}
.token-row:last-child {
border-bottom: none;
}
.token-swatch {
width: 20px;
height: 20px;
border-radius: 0.25rem;
border: 1px solid var(--color-border-strong);
flex-shrink: 0;
}
.token-row code {
flex: 1;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.6875rem;
color: var(--color-text-secondary);
}
.token-value {
font-family: "SF Mono", "Fira Code", monospace;
font-size: 0.625rem;
color: var(--color-text-muted);
}
/* โโ Sample components โโ */
.sample-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 1rem;
}
.sample-card {
background: var(--color-bg-surface);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem;
padding: 1.25rem;
transition: background var(--transition), border-color var(--transition);
}
.sample-card.elevated {
background: var(--color-bg-elevated);
}
.sample-card-title {
font-size: 0.9375rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--color-text-primary);
}
.sample-card-body {
font-size: 0.8125rem;
color: var(--color-text-secondary);
line-height: 1.5;
margin-bottom: 1rem;
}
.sample-card-body code {
font-size: 0.6875rem;
background: var(--color-bg-overlay);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
}
.sample-btn-primary {
background: var(--color-accent);
color: var(--color-accent-text);
border: none;
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
}
.sample-btn-outline {
background: transparent;
color: var(--color-accent);
border: 1px solid var(--color-accent);
border-radius: 0.375rem;
padding: 0.5rem 1rem;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
}
/* โโ Sample alerts โโ */
.sample-alerts {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.sample-alert {
padding: 0.75rem 1rem;
border-radius: 0.5rem;
font-size: 0.8125rem;
line-height: 1.5;
border-left: 3px solid;
background: var(--color-bg-surface);
color: var(--color-text-secondary);
transition: background var(--transition);
}
.sample-alert.success {
border-left-color: var(--color-success);
}
.sample-alert.warning {
border-left-color: var(--color-warning);
}
.sample-alert.error {
border-left-color: var(--color-error);
}
.sample-alert.info {
border-left-color: var(--color-info);
}
.sample-alert.success strong {
color: var(--color-success);
}
.sample-alert.warning strong {
color: var(--color-warning);
}
.sample-alert.error strong {
color: var(--color-error);
}
.sample-alert.info strong {
color: var(--color-info);
}
/* โโ Contrast matrix โโ */
.matrix-wrap {
overflow-x: auto;
}
.matrix {
border-collapse: collapse;
font-size: 0.6875rem;
width: 100%;
min-width: 500px;
}
.matrix th,
.matrix td {
padding: 0.375rem 0.5rem;
border: 1px solid var(--color-border-default);
text-align: center;
white-space: nowrap;
}
.matrix th {
background: var(--color-bg-surface);
font-weight: 600;
color: var(--color-text-secondary);
font-size: 0.5625rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.matrix th:first-child {
text-align: left;
}
.matrix td:first-child {
text-align: left;
font-weight: 600;
background: var(--color-bg-surface);
color: var(--color-text-secondary);
font-size: 0.5625rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.matrix .cell-aaa {
background: #052e16;
color: #4ade80;
font-weight: 700;
}
.matrix .cell-aa {
background: #422006;
color: #fbbf24;
font-weight: 700;
}
.matrix .cell-fail {
background: #1a0a0a;
color: #737373;
}
.light .matrix .cell-aaa {
background: #dcfce7;
color: #15803d;
}
.light .matrix .cell-aa {
background: #fef3c7;
color: #92400e;
}
.light .matrix .cell-fail {
background: #fef2f2;
color: #a3a3a3;
}
@media (max-width: 640px) {
.token-grid {
grid-template-columns: 1fr;
}
.sample-grid {
grid-template-columns: 1fr;
}
.sample-alerts {
grid-template-columns: 1fr;
}
}(function () {
var toggleBtn = document.getElementById("theme-toggle");
var themeLabel = document.getElementById("theme-label");
var body = document.body;
// โโ Token values for each theme โโ
var tokens = {
dark: {
"--color-text-primary": "#e5e5e5",
"--color-text-secondary": "#a3a3a3",
"--color-text-muted": "#737373",
"--color-text-inverse": "#0a0a0a",
"--color-bg-base": "#0a0a0a",
"--color-bg-surface": "#141414",
"--color-bg-elevated": "#1a1a1a",
"--color-bg-overlay": "#262626",
"--color-border-default": "#262626",
"--color-border-strong": "#404040",
"--color-accent": "#3b82f6",
"--color-accent-text": "#ffffff",
"--color-success": "#4ade80",
"--color-warning": "#fbbf24",
"--color-error": "#f87171",
"--color-info": "#60a5fa",
},
light: {
"--color-text-primary": "#111111",
"--color-text-secondary": "#525252",
"--color-text-muted": "#737373",
"--color-text-inverse": "#ffffff",
"--color-bg-base": "#ffffff",
"--color-bg-surface": "#f5f5f5",
"--color-bg-elevated": "#fafafa",
"--color-bg-overlay": "#e5e5e5",
"--color-border-default": "#e5e5e5",
"--color-border-strong": "#d4d4d4",
"--color-accent": "#2563eb",
"--color-accent-text": "#ffffff",
"--color-success": "#16a34a",
"--color-warning": "#d97706",
"--color-error": "#dc2626",
"--color-info": "#2563eb",
},
};
var textTokens = [
"--color-text-primary",
"--color-text-secondary",
"--color-text-muted",
"--color-text-inverse",
"--color-accent",
"--color-success",
"--color-warning",
"--color-error",
"--color-info",
];
var bgTokens = [
"--color-bg-base",
"--color-bg-surface",
"--color-bg-elevated",
"--color-bg-overlay",
];
var currentTheme = "dark";
// โโ WCAG helpers โโ
function hexToRgb(hex) {
hex = hex.replace("#", "");
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
var n = parseInt(hex, 16);
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function linearize(c) {
var s = c / 255;
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
}
function luminance(rgb) {
return 0.2126 * linearize(rgb[0]) + 0.7152 * linearize(rgb[1]) + 0.0722 * linearize(rgb[2]);
}
function contrastRatio(hex1, hex2) {
var l1 = luminance(hexToRgb(hex1));
var l2 = luminance(hexToRgb(hex2));
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}
function shortName(token) {
return token.replace("--color-", "");
}
// โโ Update token swatches โโ
function updateSwatches() {
var t = tokens[currentTheme];
var rows = document.querySelectorAll(".token-row[data-token]");
rows.forEach(function (row) {
var name = row.getAttribute("data-token");
var val = t[name];
if (!val) return;
row.querySelector(".token-swatch").style.background = val;
row.querySelector(".token-value").textContent = val;
});
}
// โโ Build contrast matrix โโ
function buildMatrix() {
var t = tokens[currentTheme];
var thead = document.getElementById("matrix-head");
var tbody = document.getElementById("matrix-body");
// Header row
var headerHtml = "<tr><th></th>";
bgTokens.forEach(function (bg) {
headerHtml += "<th>" + shortName(bg) + "</th>";
});
headerHtml += "</tr>";
thead.innerHTML = headerHtml;
// Body rows
tbody.innerHTML = "";
textTokens.forEach(function (fg) {
var tr = document.createElement("tr");
var rowHtml = "<td>" + shortName(fg) + "</td>";
bgTokens.forEach(function (bg) {
var ratio = contrastRatio(t[fg], t[bg]);
var cls, label;
if (ratio >= 7) {
cls = "cell-aaa";
label = ratio.toFixed(1) + " AAA";
} else if (ratio >= 4.5) {
cls = "cell-aa";
label = ratio.toFixed(1) + " AA";
} else {
cls = "cell-fail";
label = ratio.toFixed(1);
}
rowHtml += '<td class="' + cls + '">' + label + "</td>";
});
tr.innerHTML = rowHtml;
tbody.appendChild(tr);
});
}
// โโ Apply theme โโ
function applyTheme(theme) {
currentTheme = theme;
if (theme === "light") {
body.classList.add("light");
} else {
body.classList.remove("light");
}
themeLabel.textContent = theme === "light" ? "Dark" : "Light";
updateSwatches();
buildMatrix();
}
// โโ Events โโ
toggleBtn.addEventListener("click", function () {
applyTheme(currentTheme === "dark" ? "light" : "dark");
});
// โโ Init โโ
applyTheme("dark");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Accessible Color Token System</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="header">
<div>
<h1 class="header-title">Color Token System</h1>
<p class="header-sub">Semantic design tokens with guaranteed WCAG contrast compliance.</p>
</div>
<button class="theme-btn" id="theme-toggle" aria-label="Toggle dark/light theme">
<svg class="icon-sun" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
<svg class="icon-moon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<span id="theme-label">Light</span>
</button>
</header>
<!-- Token reference -->
<section class="section">
<h2 class="section-title">Token Reference</h2>
<div class="token-grid" id="token-grid">
<!-- Text tokens -->
<div class="token-category">
<h3 class="token-cat-title">Text</h3>
<div class="token-row" data-token="--color-text-primary">
<span class="token-swatch"></span>
<code>--color-text-primary</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-text-secondary">
<span class="token-swatch"></span>
<code>--color-text-secondary</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-text-muted">
<span class="token-swatch"></span>
<code>--color-text-muted</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-text-inverse">
<span class="token-swatch"></span>
<code>--color-text-inverse</code>
<span class="token-value"></span>
</div>
</div>
<!-- Background tokens -->
<div class="token-category">
<h3 class="token-cat-title">Backgrounds</h3>
<div class="token-row" data-token="--color-bg-base">
<span class="token-swatch"></span>
<code>--color-bg-base</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-bg-surface">
<span class="token-swatch"></span>
<code>--color-bg-surface</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-bg-elevated">
<span class="token-swatch"></span>
<code>--color-bg-elevated</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-bg-overlay">
<span class="token-swatch"></span>
<code>--color-bg-overlay</code>
<span class="token-value"></span>
</div>
</div>
<!-- Border & accent tokens -->
<div class="token-category">
<h3 class="token-cat-title">Borders & Accents</h3>
<div class="token-row" data-token="--color-border-default">
<span class="token-swatch"></span>
<code>--color-border-default</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-border-strong">
<span class="token-swatch"></span>
<code>--color-border-strong</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-accent">
<span class="token-swatch"></span>
<code>--color-accent</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-accent-text">
<span class="token-swatch"></span>
<code>--color-accent-text</code>
<span class="token-value"></span>
</div>
</div>
<!-- State tokens -->
<div class="token-category">
<h3 class="token-cat-title">States</h3>
<div class="token-row" data-token="--color-success">
<span class="token-swatch"></span>
<code>--color-success</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-warning">
<span class="token-swatch"></span>
<code>--color-warning</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-error">
<span class="token-swatch"></span>
<code>--color-error</code>
<span class="token-value"></span>
</div>
<div class="token-row" data-token="--color-info">
<span class="token-swatch"></span>
<code>--color-info</code>
<span class="token-value"></span>
</div>
</div>
</div>
</section>
<!-- Sample components -->
<section class="section">
<h2 class="section-title">Components Using Tokens</h2>
<div class="sample-grid">
<div class="sample-card">
<h3 class="sample-card-title">Surface Card</h3>
<p class="sample-card-body">This card uses <code>--color-bg-surface</code> as background and <code>--color-text-primary</code> for heading text. The body uses <code>--color-text-secondary</code>.</p>
<button class="sample-btn-primary">Primary Action</button>
</div>
<div class="sample-card elevated">
<h3 class="sample-card-title">Elevated Card</h3>
<p class="sample-card-body">Elevated surface using <code>--color-bg-elevated</code>. Borders use <code>--color-border-default</code>. Button uses <code>--color-accent</code>.</p>
<button class="sample-btn-outline">Outline Action</button>
</div>
</div>
<!-- Alerts -->
<div class="sample-alerts">
<div class="sample-alert success">
<strong>Success:</strong> Changes saved successfully.
</div>
<div class="sample-alert warning">
<strong>Warning:</strong> API rate limit at 85%.
</div>
<div class="sample-alert error">
<strong>Error:</strong> Unable to connect to database.
</div>
<div class="sample-alert info">
<strong>Info:</strong> New version available (v2.4.0).
</div>
</div>
</section>
<!-- Contrast matrix -->
<section class="section">
<h2 class="section-title">Contrast Matrix</h2>
<p class="section-desc">Shows which text/background token pairs meet WCAG AA (4.5:1) and AAA (7:1).</p>
<div class="matrix-wrap">
<table class="matrix" id="contrast-matrix" aria-label="Contrast compliance matrix">
<thead id="matrix-head"></thead>
<tbody id="matrix-body"></tbody>
</table>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>A complete design token system using CSS custom properties with semantic naming conventions. Includes text, background, border, accent, and state color categories, all designed for guaranteed WCAG contrast compliance with a contrast matrix showing valid token pairs.