whiskeyjack
libaccent-foregroundsource: ok · refreshed
applyAccentColor
Retint the whole UI from one accent color, with a WCAG-legible foreground picked automatically.
npx shadcn add "https://whiskeyjack.net/r/accent-foreground.json"free
No selection tags yet — empty means no evidence, not lack of support.
Files1 · source cached 10/8/2026, 4:23:50 AM
lib/accent-foreground.ts
/**
* Runtime accent color utilities.
*
* applyAccentForeground() -- reads --color-accent-500, computes WCAG luminance,
* and sets the 7 --color-accent-foreground* variables on :root.
*
* applyAccentColor(hex) -- generates a full accent scale (50-700) from a base
* hex color, sets all --color-accent-* CSS variables, then computes foreground.
* Used by Tauri desktop apps to apply the OS system accent color.
*
* Default foreground: white (#ffffff). Switches to dark (#1d1d1d) when the
* accent luminance > 0.30 (white contrast drops below 3:1).
*/
const LIGHT = '#ffffff'
const DARK = '#1d1d1d'
function hexToLuminance(hex: string): number {
const r = parseInt(hex.slice(1, 3), 16) / 255
const g = parseInt(hex.slice(3, 5), 16) / 255
const b = parseInt(hex.slice(5, 7), 16) / 255
const toLinear = (c: number) => c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b)
}
function hexToRgba(hex: string, alpha: number): string {
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
function parseHex(hex: string): [number, number, number] {
return [
parseInt(hex.slice(1, 3), 16),
parseInt(hex.slice(3, 5), 16),
parseInt(hex.slice(5, 7), 16),
]
}
function toHex(r: number, g: number, b: number): string {
const clamp = (v: number) => Math.round(Math.min(255, Math.max(0, v)))
return `#${clamp(r).toString(16).padStart(2, '0')}${clamp(g).toString(16).padStart(2, '0')}${clamp(b).toString(16).padStart(2, '0')}`
}
/** Mix a color channel toward a target by a given ratio (0-1). */
function mix(base: number, target: number, ratio: number): number {
return base + (target - base) * ratio
}
/**
* Generates a 6-stop accent scale from a base hex color.
* Lighter shades mix toward white, darker shades mix toward black.
*/
function generateAccentScale(hex: string) {
const [r, g, b] = parseHex(hex)
return {
50: toHex(mix(r, 255, 0.90), mix(g, 255, 0.90), mix(b, 255, 0.90)),
100: toHex(mix(r, 255, 0.80), mix(g, 255, 0.80), mix(b, 255, 0.80)),
400: toHex(mix(r, 255, 0.20), mix(g, 255, 0.20), mix(b, 255, 0.20)),
500: hex,
600: toHex(mix(r, 0, 0.15), mix(g, 0, 0.15), mix(b, 0, 0.15)),
700: toHex(mix(r, 0, 0.30), mix(g, 0, 0.30), mix(b, 0, 0.30)),
}
}
/**
* Reads --color-accent-500 and sets all accent foreground CSS variables on :root.
* Safe to call multiple times (e.g. when the accent color changes).
*/
export function applyAccentForeground(): void {
const root = document.documentElement
const accent = getComputedStyle(root).getPropertyValue('--color-accent-500').trim()
if (!accent || !accent.startsWith('#')) return
const L = hexToLuminance(accent)
const fg = L > 0.30 ? DARK : LIGHT
root.style.setProperty('--color-accent-foreground', fg)
root.style.setProperty('--color-accent-foreground-10', hexToRgba(fg, 0.1))
root.style.setProperty('--color-accent-foreground-20', hexToRgba(fg, 0.2))
root.style.setProperty('--color-accent-foreground-80', hexToRgba(fg, 0.8))
root.style.setProperty('--color-accent-foreground-icon', fg)
root.style.setProperty('--color-accent-foreground-icon-10', hexToRgba(fg, 0.1))
root.style.setProperty('--color-accent-foreground-icon-20', hexToRgba(fg, 0.2))
}
/**
* Applies a full accent color scale from a base hex, then computes foreground.
* Used by Tauri desktop apps to apply the OS system accent color at runtime.
*/
export function applyAccentColor(hex: string): void {
if (!hex || !hex.startsWith('#')) return
const root = document.documentElement
const scale = generateAccentScale(hex)
root.style.setProperty('--color-accent-50', scale[50])
root.style.setProperty('--color-accent-100', scale[100])
root.style.setProperty('--color-accent-400', scale[400])
root.style.setProperty('--color-accent-500', scale[500])
root.style.setProperty('--color-accent-600', scale[600])
root.style.setProperty('--color-accent-700', scale[700])
applyAccentForeground()
}
/**
* Removes inline --color-accent-* overrides so the theme CSS values take effect again.
* Then recomputes the foreground from the restored --color-accent-500.
*
* Use this to revert from a runtime accent (e.g. OS system color) back to the
* app's built-in theme accent.
*/
export function clearAccentColor(): void {
const root = document.documentElement
root.style.removeProperty('--color-accent-50')
root.style.removeProperty('--color-accent-100')
root.style.removeProperty('--color-accent-400')
root.style.removeProperty('--color-accent-500')
root.style.removeProperty('--color-accent-600')
root.style.removeProperty('--color-accent-700')
applyAccentForeground()
}