{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "accent-foreground", "type": "registry:lib", "title": "applyAccentColor", "description": "Retint the whole UI from one accent color, with a WCAG-legible foreground picked automatically.", "categories": [ "utils" ], "files": [ { "path": "lib/accent-foreground.ts", "type": "registry:lib", "target": "lib/accent-foreground.ts", "content": "/**\n * Runtime accent color utilities.\n *\n * applyAccentForeground() -- reads --color-accent-500, computes WCAG luminance,\n * and sets the 7 --color-accent-foreground* variables on :root.\n *\n * applyAccentColor(hex) -- generates a full accent scale (50-700) from a base\n * hex color, sets all --color-accent-* CSS variables, then computes foreground.\n * Used by Tauri desktop apps to apply the OS system accent color.\n *\n * Default foreground: white (#ffffff). Switches to dark (#1d1d1d) when the\n * accent luminance > 0.30 (white contrast drops below 3:1).\n */\n\nconst LIGHT = '#ffffff'\nconst DARK = '#1d1d1d'\n\nfunction hexToLuminance(hex: string): number {\n const r = parseInt(hex.slice(1, 3), 16) / 255\n const g = parseInt(hex.slice(3, 5), 16) / 255\n const b = parseInt(hex.slice(5, 7), 16) / 255\n const toLinear = (c: number) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4\n return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b)\n}\n\nfunction hexToRgba(hex: string, alpha: number): string {\n const r = parseInt(hex.slice(1, 3), 16)\n const g = parseInt(hex.slice(3, 5), 16)\n const b = parseInt(hex.slice(5, 7), 16)\n return `rgba(${r}, ${g}, ${b}, ${alpha})`\n}\n\nfunction parseHex(hex: string): [number, number, number] {\n return [\n parseInt(hex.slice(1, 3), 16),\n parseInt(hex.slice(3, 5), 16),\n parseInt(hex.slice(5, 7), 16),\n ]\n}\n\nfunction toHex(r: number, g: number, b: number): string {\n const clamp = (v: number) => Math.round(Math.min(255, Math.max(0, v)))\n return `#${clamp(r).toString(16).padStart(2, '0')}${clamp(g).toString(16).padStart(2, '0')}${clamp(b).toString(16).padStart(2, '0')}`\n}\n\n/** Mix a color channel toward a target by a given ratio (0-1). */\nfunction mix(base: number, target: number, ratio: number): number {\n return base + (target - base) * ratio\n}\n\n/**\n * Generates a 6-stop accent scale from a base hex color.\n * Lighter shades mix toward white, darker shades mix toward black.\n */\nfunction generateAccentScale(hex: string) {\n const [r, g, b] = parseHex(hex)\n return {\n 50: toHex(mix(r, 255, 0.90), mix(g, 255, 0.90), mix(b, 255, 0.90)),\n 100: toHex(mix(r, 255, 0.80), mix(g, 255, 0.80), mix(b, 255, 0.80)),\n 400: toHex(mix(r, 255, 0.20), mix(g, 255, 0.20), mix(b, 255, 0.20)),\n 500: hex,\n 600: toHex(mix(r, 0, 0.15), mix(g, 0, 0.15), mix(b, 0, 0.15)),\n 700: toHex(mix(r, 0, 0.30), mix(g, 0, 0.30), mix(b, 0, 0.30)),\n }\n}\n\n/**\n * Reads --color-accent-500 and sets all accent foreground CSS variables on :root.\n * Safe to call multiple times (e.g. when the accent color changes).\n */\nexport function applyAccentForeground(): void {\n const root = document.documentElement\n const accent = getComputedStyle(root).getPropertyValue('--color-accent-500').trim()\n if (!accent || !accent.startsWith('#')) return\n\n const L = hexToLuminance(accent)\n const fg = L > 0.30 ? DARK : LIGHT\n\n root.style.setProperty('--color-accent-foreground', fg)\n root.style.setProperty('--color-accent-foreground-10', hexToRgba(fg, 0.1))\n root.style.setProperty('--color-accent-foreground-20', hexToRgba(fg, 0.2))\n root.style.setProperty('--color-accent-foreground-80', hexToRgba(fg, 0.8))\n root.style.setProperty('--color-accent-foreground-icon', fg)\n root.style.setProperty('--color-accent-foreground-icon-10', hexToRgba(fg, 0.1))\n root.style.setProperty('--color-accent-foreground-icon-20', hexToRgba(fg, 0.2))\n}\n\n/**\n * Applies a full accent color scale from a base hex, then computes foreground.\n * Used by Tauri desktop apps to apply the OS system accent color at runtime.\n */\nexport function applyAccentColor(hex: string): void {\n if (!hex || !hex.startsWith('#')) return\n const root = document.documentElement\n const scale = generateAccentScale(hex)\n\n root.style.setProperty('--color-accent-50', scale[50])\n root.style.setProperty('--color-accent-100', scale[100])\n root.style.setProperty('--color-accent-400', scale[400])\n root.style.setProperty('--color-accent-500', scale[500])\n root.style.setProperty('--color-accent-600', scale[600])\n root.style.setProperty('--color-accent-700', scale[700])\n\n applyAccentForeground()\n}\n\n/**\n * Removes inline --color-accent-* overrides so the theme CSS values take effect again.\n * Then recomputes the foreground from the restored --color-accent-500.\n *\n * Use this to revert from a runtime accent (e.g. OS system color) back to the\n * app's built-in theme accent.\n */\nexport function clearAccentColor(): void {\n const root = document.documentElement\n root.style.removeProperty('--color-accent-50')\n root.style.removeProperty('--color-accent-100')\n root.style.removeProperty('--color-accent-400')\n root.style.removeProperty('--color-accent-500')\n root.style.removeProperty('--color-accent-600')\n root.style.removeProperty('--color-accent-700')\n\n applyAccentForeground()\n}\n" } ], "docs": "applyAccentColor derives a full --color-accent-* scale from a single hex and sets the matching foreground by WCAG luminance, so text on the accent stays legible at any hue. Pair it with ColorInput for a user-chosen accent, and call clearAccentColor to fall back to the token default.", "meta": { "group": "hooks", "related": [ "color-input", "use-theme" ], "exports": [ "applyAccentForeground", "applyAccentColor", "clearAccentColor" ], "siteSlug": "apply-accent-color" } }