🍞 Crust

Theming & icons

Crust ships a warm matte theme with automatic dark mode, and assumes you’ll want to make it yours. Every token is a CSS custom property on .crust-region: override what you need, keep the rest. Typography always inherits from your page; Crust never loads a font.

Custom properties

.crust-region {
  --crust-surface: oklch(0.98 0.005 75);
  --crust-ink: oklch(0.28 0.012 75);
  --crust-radius: 10px;
  --crust-width: 360px;
  --crust-success: oklch(0.55 0.12 150);
  --crust-error: oklch(0.55 0.15 30);
  --crust-info: oklch(0.55 0.1 250);
  --crust-dur-in: 320ms;
  --crust-dur-morph: 280ms;
  --crust-dur-out: 200ms;
}
Token groupProperties
Surfaces--crust-surface, --crust-ink, --crust-ink-muted, --crust-border, --crust-shadow
Semantics--crust-success, --crust-error, --crust-info, --crust-warning (icon strokes only)
Shape--crust-radius, --crust-width, --crust-msg-max-height, --crust-offset, --crust-z
Motion--crust-ease, --crust-ease-exit, --crust-dur-in, --crust-dur-morph, --crust-dur-out

Semantic color is deliberately confined to the icon strokes. Surfaces stay neutral, text stays ink. If you override the semantic tokens, they’ll still only color the icons.

Dark mode

Out of the box, Crust follows the operating system through prefers-color-scheme, with no setup and no flash.

If your site has its own theme toggle (a .dark class on <html> is the common pattern), take over both halves explicitly. Your stylesheet loads after Crust’s, so flat declarations win the cascade over its media query:

/* Pin the light theme regardless of OS... */
.crust-region {
  --crust-surface: oklch(0.98 0.005 75);
  --crust-ink: oklch(0.28 0.012 75);
}

/* ...and switch on your class instead. */
.dark .crust-region {
  --crust-surface: oklch(0.26 0.01 60);
  --crust-ink: oklch(0.93 0.008 80);
  --crust-border: oklch(0.35 0.012 60);
}

Two notes from experience: dark mode is not inverted light mode. Keep your hue family and let depth come from surface lightness, not shadows. And semantic colors want a little more lightness on dark surfaces (the built-in dark theme raises them for exactly this reason).

Icons

Icons accept a raw SVG string, a DOM Element (cloned per toast), or a factory function. Set them globally per type, or per toast:

import { mountToaster, toast } from '@oscarrc/crust/vanilla';
import { createElement, Croissant, Flame } from 'lucide';

mountToaster({
  icons: {
    success: () => createElement(Croissant),
    error: '<svg viewBox="0 0 24 24">...</svg>'
  }
});

toast('One-off icon', { icon: () => createElement(Flame) });
toast('No icon at all', { icon: null });

Works with lucide (vanilla) and lucide-static (strings). lucide-react JSX components won’t work because the renderer is vanilla DOM, not React.

The built-in icons are stroke-drawn with pathLength="1", which powers the draw-in animation. Custom stroke icons can opt in by adding class="crust-draw" and pathLength="1" to their paths.

Reduced motion

Under prefers-reduced-motion: reduce, every entrance, exit and morph collapses to a fade: same durations, no movement. There’s nothing to configure; it’s a theme, not a degraded fallback.