colors #
fuz_css's colors are derived: a handful of high-leverage knobs produce every color variables in pure CSS, in the oklch colorspace, adapting to the color-scheme automatically. OKLCH lightness is perceptually uniform (equal lightness reads equally light in every hue), so rotating a hue knob is safe: contrast and visual weight hold.
Hues use letters so themes can reassign colors without breaking semantics ("a" is blue by
default but could be any color). Meaning attaches through the intent knobs layered on top: --hue_accent (links, focus, selection, selected states - what other systems call
"primary") defaults to --hue_a, --hue_negative to --hue_c, and so on. Retarget an intent to move just that meaning; rotate a letter
to move the palette.
Hue variables #
Hue variables contain a single OKLCH hue angle. Because lightness and chroma are shared across all hues at each stop, the scales are interchangeable: setting a hue alone is enough, no per-hue tuning required. The one deliberate exception is the per-slot chroma multiplier - the brown slot ships muted because no hue angle renders brown at full palette chroma.
Hue variables are also useful to construct custom colors not covered by the palette. For
example, fuz_css's selection color derives from --hue_accent (try selecting some
text - same hue!).
Hue variables are the same in both light and dark modes (non-adaptive).
- 250blue · default accent
- 144green · default positive
- 29red · default negative
- 296purple
- 98yellow
- 60brown · default neutral
- 358pink
- 41orange · default caution
- 210cyan · default info
- 180teal
Palette variables #
Each hue has intensity variants from subtle to bold (00, 05, 10, 20, ..., 80, 90, 95, 100),
with 50 at the middle of the ramp. The 60 variant is the text-safe stop: links, the labels of .palette_X buttons and chips, and a
selected .palette_X button's fill use it, and check_theme gates
those pairings at AA.
Palette variables work for both text and backgrounds through utility classes: .color_a_50 sets the text color and .bg_a_50 the background, and the intent scales below do the same. The shade and text scales are split by role
instead, one for surfaces and one for text.
Palette stops are adaptive: they switch between light and dark ramps based on color scheme. There are no absolute variants; for a color that doesn't adapt, write the literal color or define one custom property.
Intent variables #
Intent scales name colors by meaning: --accent_NN, --positive_NN, --negative_NN, --caution_NN, and --info_NN, with the
palette's stops, derived through the same ramps from each intent's hue knob
(--hue_accent and the rest). Each hue binds to a palette letter by default, so an
intent scale matches its letter's until a theme rebinds it: accent → a, positive → b, negative → c, caution → h, info → i.
Reach for an intent when the color carries meaning, like an error or a success, and for a
letter when it's only a color, so a theme can move the meaning without recoloring every use of
the letter. The classes follow the palette's: .positive_50 sets the text color
and .bg_positive_50 the background. Border, outline, and shadow classes exist
only for the letters, so reach an intent there with a literal like border-color:var(--negative_50).
Color knobs #
The knobs are the theme API, and the first few carry most themes:
- intent hues -
--hue_accent,--hue_positive,--hue_negative,--hue_caution,--hue_info- each pointing a meaning at a palette letter (var(--hue_c)) or a literal angle, and deriving its full stop scale (--accent_00…--accent_100) --hue_neutral+--neutral_chroma- the temperature and strength of every surface, text, border, and shadow tint (the neutral intent, whose scales are the shade and text ramps)--chroma_scale- one multiplier from grayscale (0) through calm (1) to vivid (above 1, deliberately clipping the weakest hues)- lightness ramps -
--palette_lightness_00/_100/_curve(and the same trio forshade_andtext_): the endpoint stops plus a curve exponent bending the ramp between them, per color scheme - chroma curve -
--palette_chroma_min/_maxand--chroma_curve: a mid-peaked curve, clamped per stop by gamut caps computed from the worst hue - per-slot chroma multipliers -
--palette_a_chroma_scale…--palette_j_chroma_scaleand intent twins (--accent_chroma_scale, …), each multiplying one slot's chroma under--chroma_scale. A binding shares only the hue angle, so an intent bound to the muted brown slot needs its twin set too, andvalidate_themewarns when it isn't --hue_a…--hue_j- the palette letters' angles. Rotating one recolors every use of that letter, so registered themes leave them alone and move intents instead
Every intermediate value these produce is also its own variable
(--palette_lightness_30, --palette_chroma_50, …), so a theme can pin
any individual stop as an escape hatch.
// a warm, slightly vivid theme in three moves
const warm_theme: Theme = {
name: 'warm',
variables: [
{name: 'hue_neutral', light: '55'},
{name: 'neutral_chroma', light: '0.03'},
{name: 'chroma_scale', light: '1.15'},
],
};