Theming
The library is themed entirely through CSS custom properties named --myghf-*. The Tailwind preset maps each one to a utility class, so changing a token restyles every component that consumes it.
The token model
Tokens store space-separated RGB channels — not hex and not a full rgb() value:
:root {
--myghf-primary-500: 0 162 221; /* MYGHF Blue */
--myghf-background: 248 249 250; /* app background */
}The preset wraps each token with Tailwind's <alpha-value> placeholder:
colors: {
primary: {
500: 'rgb(var(--myghf-primary-500) / <alpha-value>)',
},
background: 'rgb(var(--myghf-background) / <alpha-value>)',
}Two consequences follow:
- Ordinary utilities work directly:
bg-primary-500,text-foreground,border-border,bg-surface. - Opacity modifiers work, because Tailwind substitutes
<alpha-value>:bg-primary-500/50,text-foreground/70,border-border/60.
Brand scales and semantic tokens
- Brand scales —
primary,secondary,success,warning,error,info— each run50through900, with500as the brand value. - Semantic tokens —
background,foreground,surface,surface-muted,border,muted— describe roles rather than hues, so semantic utilities adapt to dark mode with no extra classes.
Overriding tokens
Override any --myghf-* variable after importing tokens.css:
:root {
--myghf-primary-500: 0 130 177; /* custom brand blue */
--myghf-background: 255 255 255; /* strict brand white */
}Because the dark block does not re-declare the brand scales (see below), a :root override keeps applying in dark mode. Prefer overriding tokens to patching component class names.
Dark mode
tokens.css ships the dark semantic layer under both selectors:
[data-theme='dark'],
.dark {
color-scheme: dark;
--myghf-background: 15 18 23;
--myghf-foreground: 243 244 246;
--myghf-surface: 26 30 37;
--myghf-surface-muted: 39 44 53;
--myghf-border: 55 61 71;
--myghf-muted: 156 163 175;
}| Token | Light (:root) | Dark |
|---|---|---|
--myghf-background | 248 249 250 | 15 18 23 |
--myghf-foreground | 0 0 0 | 243 244 246 |
--myghf-surface | 255 255 255 | 26 30 37 |
--myghf-surface-muted | 243 244 246 | 39 44 53 |
--myghf-border | 230 230 230 | 55 61 71 |
--myghf-muted | 128 128 128 | 156 163 175 |
Three rules matter:
- The dark block changes only the six semantic tokens. Brand scales are not re-declared, so a consumer's
:rootoverrides still apply whenever dark mode is active. - Brand tints follow the
*-900/40+*-200rule. On dark, a light tint such asbg-primary-100pairs withdark:bg-primary-900/40 dark:text-primary-200(anddark:border-primary-700for outlines). This is codified intoneClassesand used byTag,Alert/Message, andToast. dark:utilities require the.darkclass. Settingdata-theme="dark"by hand drives the token block but leaves everydark:variant inactive.useThemewrites both so the two always agree — if you apply a theme yourself, addclass="dark"as well.
Components that style with semantic utilities (bg-surface, text-foreground, border-border) adapt automatically. Only tinted or edge-case components need explicit dark: variants. Toggle this site's appearance and watch the tokens respond:
surface
Cards and panels
surface-muted
Recessed areas and hovers
primary tint
Brand tints follow the *-900/40 + *-200 rule.
<template>
<div class="grid gap-3 sm:grid-cols-3">
<div class="rounded-lg border border-border bg-surface p-4">
<p class="text-sm font-medium text-foreground">surface</p>
<p class="mt-1 text-xs text-muted">Cards and panels</p>
</div>
<div class="rounded-lg border border-border bg-surface-muted p-4">
<p class="text-sm font-medium text-foreground">surface-muted</p>
<p class="mt-1 text-xs text-muted">Recessed areas and hovers</p>
</div>
<div
class="rounded-lg border border-primary-300 bg-primary-100 p-4 dark:border-primary-700 dark:bg-primary-900/40"
>
<p class="text-sm font-medium text-primary-800 dark:text-primary-200">primary tint</p>
<p class="mt-1 text-xs text-primary-800 dark:text-primary-200">
Brand tints follow the <code>*-900/40</code> + <code>*-200</code> rule.
</p>
</div>
</div>
</template>useTheme and createTheme
useTheme(options?) returns a shared singleton controller; createTheme(options?) returns an independent one. Both are safe to call during SSR. Without a DOM the initial mode is defaultMode and apply()/persist() are no-ops; resolved is 'light' only when that mode resolves light ('light' or 'system'), so createTheme({ defaultMode: 'dark' }) resolves to 'dark'.
import { useTheme } from '@myghf/ui'
const { mode, resolved, isDark, setMode, toggle, enable, disable, reset } = useTheme()| Option | Default | Meaning |
|---|---|---|
storageKey | 'myghf-theme' | localStorage key used to persist an explicit mode |
attribute | 'both' | Which attribute(s) to write on <html>: 'both', 'class', or 'data-theme' |
defaultMode | 'system' | Mode used when nothing is stored |
mode is 'light' | 'dark' | 'system'. resolved collapses 'system' against the prefers-color-scheme media query, and isDark is resolved === 'dark'. setMode records an explicit choice (and persists it), while toggle, enable, and disable are shorthands. reset() returns to 'system' and clears the stored value.
import { createTheme } from '@myghf/ui'
const theme = createTheme({ storageKey: 'acme-theme', attribute: 'class' })In development, calling
useTheme()a second time with different options logs a warning and reuses the first instance. Reach forcreateTheme()when you need different options.
ThemeToggle
ThemeToggle is a ghost-styled button that flips the shared theme and reflects state with aria-pressed:
<ThemeToggle />| Prop | Type | Default |
|---|---|---|
size | 'sm' | 'default' | 'lg' | 'default' |
label | string | 'Toggle theme' (also the accessible name) |
<script setup lang="ts">
import { ThemeToggle } from '@myghf/ui'
</script>
<template>
<!-- ThemeToggle reads matchMedia/localStorage, so keep it client-only on an SSR site. -->
<ClientOnly>
<div class="flex flex-wrap items-center gap-4">
<ThemeToggle />
<span class="text-sm text-muted">
Writes both <code>.dark</code> and <code>data-theme="dark"</code> on
<code><html></code>.
</span>
</div>
</ClientOnly>
</template>