shadimage
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()
}

More from whiskeyjack

Browse all components in this library →