variables #
Style variables, or just "variables" in fuz_css, are CSS custom properties that can be grouped into a theme. Each variable has a base value that applies in both color-schemes, a dark value that
replaces it in dark mode, or both. They're design tokens with an API.
The goal of the variables system is to provide runtime theming that's efficient and ergonomic for both developers and end-users. Variables can be composed in multiple ways:
- by CSS classes, both utility and component
- by other variables, both in calculations and to add useful semantics (e.g.
button_fill_hoverusesshade_50but can be themed independently) - in JS like the Svelte components in fuz_ui
Variables also provide an interface that's generally secure for user-generated content, if you're into that kind of thing.
The result is a flexible system that aligns with modern CSS to deliver high-capability UX and DX with low overhead.
Most color variables are derived: knobs feed ramp stops and ramp stops
feed color stops, all computed in pure CSS
(calc()/pow()/oklch()). Scale knobs like radius_scale derive other families the same way, and a derived token or color
stop can still be pinned as an escape hatch. The ramp stops in between are internal
(--_palette_lightness_30), so a theme can't set them. See themes for setting the rest.
In bundled mode, only the variables your code uses are emitted, along with any they depend on. The full theme.css stylesheet ships every variable, for utility-only mode and direct imports.
export type Theme = {
name: string;
/** What the theme looks like, for theme pickers. */
summary?: string;
variables: StyleVariable[];
/** Single-scheme themes render one appearance in both color schemes. */
scheme?: 'dual' | 'light' | 'dark';
};
export type StyleVariable = {
name: string;
base?: string;
dark?: string;
summary?: string;
}; Both are @fuzdev/fuz_schema schemas, so the same names validate at runtime: s.parse(Theme, value) reports what's wrong, parse_theme(value) returns a theme or null, and validate_theme(theme) in theme_validate.ts runs the full lint.
Hooks #
Hooks are the contextual surface: custom properties the base styles and classes read but don't
declare as variables, so they have no :root default. Set one on an element to
retune what reads it there, with a literal class like --flow_margin:0 or in a
component's styles - the sized_* composites work this way, setting --flow_margin and --font_size for their subtree. Where nothing sets
a hook, its read falls back to the value listed. A local hook is declared by the rule
that reads it, so a value reaches it only from a rule on the same element.
Hooks aren't theme variables: from a theme's root, an inherited hook retunes every element
that reads it and a local one reaches nothing, so validate_theme warns on a theme
that sets one. Themes turn the knobs instead. The catalog is style_hooks.ts.
| hook | read by | fallback |
|---|---|---|
--flow_margin | block elements, headings, .mb_flow, .mt_flow | var(--space_lg) |
--font_size | headings, .heading, button, input[type='checkbox']::before, .chip | inheritvar(--font_size_xl3)revert-layer |
--icon_size | .menuitem .icon | var(--icon_size_md) |
--menuitem_padding | .menuitem | var(--space_xs3) var(--space_xs) |
--menuitem_min_height | .menuitem | var(--input_height_compact) |
--chip_padding_x | .chip | var(--space_xs) |
--border_radius | a, aside, code, summary, input, textarea, select, button, .pane, .panel, .chip | var(--border_radius_xs3)var(--border_radius_xs)var(--control_radius) |
--fill | button | var(--shade_50)var(--shade_60) |
--button_fill | button, .selectable | local |
--button_fill_hover | button, .selectable | local |
--button_fill_active | button, .selectable | local |
--button_text_color | button | local |
--button_border_color | button | local |
--button_border_color_hover | button | local |
--button_border_color_active | button | local |
--button_min_height | button, button.inline, p button | var(--input_height)var(--input_height_compact) |
--shadow | button | var(--button_shadow) |
--select_arrow_size | select | local |
--thumb_size | input[type='range'] | local |
--thumb_background_color | input[type='range'] | local |
--checkbox_content | input[type='checkbox']::before | local |
--checkbox_content_empty | input[type='checkbox'] | '' |
--checkbox_content_checked | input[type='checkbox'] | '✓' |
--overflow | input[type='checkbox']::before | visible |
--clickable_transform | .clickable | scale3d(1, 1, 1) |
--clickable_transform_focus | .clickable | scale3d(1.07, 1.07, 1.07) |
--clickable_transform_hover | .clickable | scale3d(1.1, 1.1, 1.1) |
--clickable_transform_active | .clickable | scale3d(1.2, 1.2, 1.2) |
--clickable_transform_origin | .clickable | center |
--clickable_transition_duration | .clickable | 0s |
--shadow_color | .shadow_*, .shadow_top_*, .shadow_bottom_*, .shadow_inset_* | var(--shadow_color_umbra) |
--shadow_alpha | .shadow_*, .shadow_top_*, .shadow_bottom_*, .shadow_inset_* | var(--shadow_alpha_30)var(--shadow_alpha_40)var(--shadow_alpha_50)var(--shadow_alpha_60)var(--shadow_alpha_70) |