fuz_css
CSS with more utility 🪴
CSS framework and design system for semantic HTML
npm i -D @fuzdev/fuz_css
introduction #
fuz_css is a CSS framework and design system for semantic HTML. It styles elements by default and integrates custom properties, themes, and utility classes into a complete system. It's Svelte-first but works with plain HTML/JS/TS, React, Preact, Solid, and other JSX frameworks. For more see the framework support docs, and for the companion Svelte components, see fuz_ui.
The only required parts are a reset stylesheet with the semantic defaults and a replaceable theme stylesheet containing the variables used in the reset, and these require no dependencies. There's also utility classes for composition and convenience with a Vite plugin, and the library exports the full API for complex usage.
Usage #
npm i -D @fuzdev/fuz_css Import the two required stylesheets:
import '@fuzdev/fuz_css/style.css';
import '@fuzdev/fuz_css/theme.css'; // or bring your own For utility classes, see the classes reference which covers the Vite plugin and alternative Gro generator.
Details #
- plain CSS
- minimal dependencies, all optional -- none needed if you only use the stylesheets
- exports a reset stylesheet with semantic defaults that styles HTML elements, and also exports the underlying data, helpers, and types for open-ended usage
- supports themes with a basic theme stylesheet, 🗎 @fuzdev/fuz_css/theme.css, that can be replaced with your own -- dark mode is a first-class concept, not a theme; instead, each theme can support light and/or dark color-schemes
- supports optional utility classes with three types (token, composite, CSS-literal) and modifiers for responsive, state, color-scheme, and pseudo-elements
- uses its own concept of style variables, a specialization of CSS custom properties and design tokens that integrate with the other systems (e.g. the reset stylesheet and token classes use variables, and themes are groups of variables)
- the stylesheets work with any framework and plain HTML; utility class generation supports
Svelte, JSX, and TypeScript/JS -- see the utility class framework support, and for
the companion Svelte integration see
Themedin fuz_ui - see the comparison to alternatives to understand fuz_css relative to TailwindCSS and UnoCSS
api #
Browse the full api docs.
examples #
The example repos demonstrate the classes system using the Vite plugin:
- vite-svelte - Svelte 5
- vite-react - React 19
- vite-preact - Preact
- vite-solid - Solid
semantic #
fuz_css styles HTML elements in its 🗎 reset stylesheet, so semantic markup gets themed and color-scheme-aware styling automatically -- utility classes optional. The goal is to be accessible and attractive out of the box, minimal yet extensible.
Low specificity #
All opinionated styles use :where() selectors, giving them zero specificity beyond
the element itself. Your styles and utility classes override defaults without specificity battles.
/* any styles you apply will override these */
:where(a:not(.unstyled)) {
color: var(--link_color);
font-weight: 700;
}
:where(button:not(.unstyled)) {
background-color: var(--button_fill);
border-radius: var(--border_radius_sm);
}.unstyled escape hatch #
Add .unstyled to opt out of decorative styling while keeping reset normalizations.
Works for both decorative containers and interactive elements like links, buttons, inputs, and summary.
<a href="/home">styled link</a>
<a href="/home" class="unstyled">unstyled link</a> <button>styled button</button>
<button class="unstyled">unstyled button</button>
Document flow #
Block elements get margin-bottom via :not(:last-child), creating
natural vertical rhythm without trailing margins.
:where(
:is(p, ul, ...[many others])
:not(:last-child):not(.unstyled)
) {
margin-bottom: var(--space_lg);
} This eliminates bottom margins on terminal elements. Edge cases can be fixed with .mb_lg or similar utility classes.
Element-specific docs #
See the related docs for specifics:
- buttons - button states, colors, variants
- elements - links, lists, tables, code, details
- forms - inputs, labels, checkboxes, selects
- typography - headings, fonts, text styles
themes #
fuz_css supports both the browser's color-scheme and custom themes based on variables, which use CSS custom properties.
fuz_css works with any JS framework, but it provides only stylesheets, not integrations. This website uses the companion Svelte UI library fuz_ui to provide the UI below to control the fuz_css color scheme and themes.
Color scheme #
fuz_css supports color-scheme with dark and light modes. To apply dark mode manually,
add the dark class to the root html element.
The Fuz integration detects the default with prefers-color-scheme, and users can also set it directly with a component like this one:
The builtin themes support both dark and light color schemes. Custom themes may support one or both color schemes.
Builtin themes #
A theme is a simple JSON collection of variables that can be transformed into CSS that set custom properties. Each variable can have values for light and/or dark color schemes. In other words, "dark" isn't a theme, it's a mode that any theme can implement.
These docs are a work in progress, for now see theme.ts and themes.ts.
variables #
Style variables, or just "variables" in fuz_css, are CSS custom properties that can be grouped into a theme. Each variable can have values for light and/or dark color-schemes. They're design tokens with an API.
The goal of the variables system is to provide runtime theming that's efficient and ergnomic 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_hoverdefaults tofg_2but 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.
export interface Theme {
name: string;
variables: StyleVariable[];
}
export interface StyleVariable {
name: string;
light?: string;
dark?: string;
summary?: string;
}All 335 style variables #
classes #
fuz_css has two categories of CSS classes: utilities and builtins. Builtins are baked into the main stylesheet; utility classes have three types, are optional, and require build tool integration.
Utility classes complement semantic styles and style variables. Use them to compose styles across
component boundaries, or when you prefer classes to the <style> tag and style attribute. They're optional and generated on-demand to include only what you use.
Compared to TailwindCSS and UnoCSS, fuz_css utility classes follow the grain of semantic HTML rather than being foundational to the design, and the DSL is currently more limited, with interpreters providing a programmatic escape hatch -- see the comparison below.
Compared to the <style> tag, classes:
- offer shorthand for style variables (
p_lgvspadding: var(--space_lg)) - compose across component boundaries, avoiding fragile
:global()selectors - let you avoid noisy class names like
foo-wrapperandbar-inner
Compared to the style attribute, classes:
- support powerful modifiers for responsive widths, interaction states (like hover), and dark mode
- provide more control over specificity
- compose ergonomically with libraries like clsx, which Svelte supports natively
For cases where classes lack clear advantages, style and <style> are simpler and avoid generating class definitions, which can bloat your builds when overused.
Utility class types #
Token classes #
Token classes are technically composite classes with a close relationship to style variables -- each maps design tokens to CSS properties. They're generated programmatically from variant data, making them predictable and systematic. The composites documented below are hand-written and typically represent higher-level semantic concepts. For raw CSS values, use literal classes instead.
<p class="pl_xl3 color_g_5">some token classes</p> some token classes
Token classes use snake_case because style variables are designed for optional
use in JS (imported from variables.ts, but costing nothing
otherwise), so each name is consistent across both JS and CSS, instead of converting between kebab-case and camelCase. This also makes token classes visually distinct from literal classes; we find this improves readability.
Spacing
See layout.
.p_{xs5-xl15}.p_0.pt_{xs5-xl15}.pt_0.pr_{xs5-xl15}.pr_0.pb_{xs5-xl15}.pb_0.pl_{xs5-xl15}.pl_0.px_{xs5-xl15}.px_0.py_{xs5-xl15}.py_0.m_{xs5-xl15}.m_0.m_auto.mt_{xs5-xl15}.mt_0.mt_auto.mr_{xs5-xl15}.mr_0.mr_auto.mb_{xs5-xl15}.mb_0.mb_auto.ml_{xs5-xl15}.ml_0.ml_auto.mx_{xs5-xl15}.mx_0.mx_auto.my_{xs5-xl15}.my_0.my_auto.gap_{xs5-xl15}.column_gap_{xs5-xl15}.row_gap_{xs5-xl15}.top_{xs5-xl15}.right_{xs5-xl15}.bottom_{xs5-xl15}.left_{xs5-xl15}.inset_{xs5-xl15}
Sizing
See layout.
.width_{xs5-xl15}.height_{xs5-xl15}.width_atmost_{xs-xl}.width_atleast_{xs-xl}.height_atmost_{xs-xl}.height_atleast_{xs-xl}
Colors
See colors.
.color_{a-j}_{1-9}.bg.fg.bg_{1-9}.fg_{1-9}.bg_{a-j}_{1-9}.text_color_{0-10}.color_bg.color_fg.color_bg_{1-9}.color_fg_{1-9}.darken_{1-9}.lighten_{1-9}.color_darken_{1-9}.color_lighten_{1-9}.hue_{a-j}
Typography
See typography.
.font_family_sans.font_family_serif.font_family_mono.font_size_{xs-xl9}.line_height_{xs-xl}.icon_size_{xs-xl3}
Borders
See borders.
.border_color_{1-5}.border_color_{a-j}.border_width_{1-9}.border_radius_{xs3-xl}.border_top_left_radius_{xs3-xl}.border_top_right_radius_{xs3-xl}.border_bottom_left_radius_{xs3-xl}.border_bottom_right_radius_{xs3-xl}.outline_width_{1-9}.outline_width_focus.outline_width_active.outline_color_{1-5}.outline_color_{a-j}
Shadows
See shadows.
.shadow_{xs-xl}.shadow_top_{xs-xl}.shadow_bottom_{xs-xl}.shadow_inset_{xs-xl}.shadow_inset_top_{xs-xl}.shadow_inset_bottom_{xs-xl}.shadow_color_{a-j}.shadow_color_highlight.shadow_color_glow.shadow_color_shroud.shadow_alpha_{1-5}
Composite classes #
Composites let you name and reuse patterns, extending the class system with your own vocabulary. They have four forms: raw CSS declarations, compositions of other classes, a combination of both, or full rulesets as an escape hatch for multi-selector patterns (child selectors, sibling combinators, etc.).
Four definition forms
All four of these produce the same CSS output for .centered, with the ruleset
form additionally demonstrating child selectors (> * + *) which can't be
expressed with the other forms.
import type {CssClassDefinition} from '@fuzdev/fuz_css/css_class_generation.js';
export const custom_composites: Record<string, CssClassDefinition> = {
// 1. `declaration` only - custom CSS properties
centered: {
declaration: `
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
`,
},
// 2. `composes` only - compose existing token/composite classes
centered: {
composes: ['box', 'text-align:center'],
},
// 3. `composes` + `declaration` - compose then extend
centered: {
composes: ['box'],
declaration: 'text-align: center;',
},
// 4. `ruleset` - full CSS with multiple selectors (not composable)
centered: {
ruleset: `
.centered {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
/* child selectors, pseudo-classes on children, etc */
.centered > * + * {
margin-top: var(--space_md);
}
`,
},
}; Generated CSS:
.centered {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
} And the ruleset form (4) includes the additional selector:
.centered > * + * {
margin-top: var(--space_md);
} Nesting
Composites can compose other composites, enabling layered abstractions. Resolution is
depth-first: nested composes are fully resolved before the parent's declaration is appended. Circular references are detected and produce an error.
What composes can reference
The composes property resolves referenced classes and combines their
declarations. When both composes and declaration are present, the explicit
declaration comes last (winning in the cascade for duplicate properties).
- token classes (
p_lg,color_a_5) - resolved to their declarations - composites with
declaration- the declaration is included - composites with
composes- recursively resolved - unmodified CSS literals (
text-align:center,margin:0~auto,--my-var:value) - parsed and included as declarations
Not allowed: Composites with ruleset cannot be referenced in composes because they define their own selectors. Modified classes (like hover:opacity:80% or md:p_lg) cannot be used in composes arrays because they require wrapper selectors (apply them directly in
markup instead). The composes property merges declarations into a single rule,
but multi-selector patterns like .clickable:hover { ... } cannot be
inlined. These limitations may be revisited in the future; feedback is welcome in the discussions.
Modifiers
Composites support modifiers like any other class. For composes and declaration composites, declarations are combined and wrapped. For ruleset composites, modifiers
are applied to each selector (with smart conflict detection):
<!-- hover:foo resolves foo's `composes`, applies :hover -->
<div class="hover:foo md:dark:foo md:clickable"> Registering composites
Register custom composites with the Vite plugin or Gro generator:
Vite plugin
// vite.config.ts
import {custom_composites} from './src/lib/composites.js';
vite_plugin_fuz_css({
class_definitions: custom_composites,
}), Gro generator
// fuz.gen.css.ts
import {custom_composites} from '$lib/composites.js';
export const gen = gen_fuz_css({
class_definitions: custom_composites,
}); See Usage for more details. Builtin composites
Composable (can be used in composes arrays):
.box- centered flex container.row- horizontal flex row.column- vertical flex column.ellipsis- text overflow ellipsis.pane- pane container.panel- panel container.icon_button- icon button styling.pixelated- crisp pixel-art rendering.circular- 50% border-radius
Ruleset-based (multi-selector, apply directly in markup):
.selectable- selectable element styling.clickable- clickable element styling.plain- plain/reset styling.menu_item- menu item styling.chevron- chevron indicator.chip- chip/tag styling
Literal classes #
Fuz supports an open-ended CSS-literal syntax: property:value. Any CSS property
and value works, offering arbitrary styles without a DSL.
<!-- basic syntax: property:value -->
<div class="display:flex justify-content:center">
<!-- multi-value properties use ~ for spaces -->
<div class="margin:1px~3rem">
<!-- numeric values -->
<div class="opacity:50% font-weight:700 z-index:100">
<!-- arbitrary CSS values -->
<div class="width:calc(100%~-~20px)">
<!-- custom properties -->
<div class="--foo-bg:#abc"> The ~ character represents a space in class names (since CSS classes can't
contain spaces). Use it for multi-value properties like margin:1px~auto.
Custom properties work directly: --my-var:value sets the property on the element.
This is useful for scoped variables or passing values to child components.
Modifiers #
Modifiers prefix any class type -- token, composite, or literal -- to apply styles conditionally based on viewport, state, or color scheme. This is what makes utility classes more powerful than inline styles.
Responsive modifiers
Mobile-first breakpoints:
| Prefix | Width | CSS |
|---|---|---|
sm: | 40rem (640px) | @media (width >= 40rem) |
md: | 48rem (768px) | @media (width >= 48rem) |
lg: | 64rem (1024px) | @media (width >= 64rem) |
xl: | 80rem (1280px) | @media (width >= 80rem) |
2xl: | 96rem (1536px) | @media (width >= 96rem) |
<!-- stack on mobile, row on medium screens and up -->
<div class="display:flex flex-direction:column md:flex-direction:row">
<!-- hide on mobile -->
<nav class="display:none md:display:flex">
<!-- max-width variant -->
<div class="max-md:display:none">
<!-- arbitrary breakpoints -->
<div class="min-width(800px):color:red max-width(600px):color:blue"> State modifiers
Pseudo-class modifiers for interaction and form states:
<button class="hover:opacity:80% focus:outline-color:blue">
<input class="disabled:opacity:50% invalid:border-color:red">
<li class="first:font-weight:bold odd:background-color:lightgray"> Available state modifiers include:
- interaction:
hover:focus:focus-visible:focus-within:active:link:visited:any-link:target: - form:
autofill:blank:disabled:enabled:checked:indeterminate:required:optional:valid:invalid:user-valid:user-invalid:in-range:out-of-range:placeholder-shown:read-only:read-write:default: - structural:
first:last:only:first-of-type:last-of-type:only-of-type:odd:even:empty:nth-child(N):nth-last-child(N):nth-of-type(N):nth-last-of-type(N): - UI states:
fullscreen:modal:open:popover-open: - media:
playing:paused:
Color-scheme modifiers
Apply styles in dark or light mode:
<div class="shadow_lg dark:shadow_sm">
<div class="color:black light:color:gray"> dark: and light: use :root.dark and :root.light selectors, matching fuz_css's color scheme mechanism.
Pseudo-element modifiers
Style generated content and element parts:
<span class="before:content:'→' before:margin-right:0.5rem">
<input class="placeholder:opacity:50%"> available: before: after: cue: first-letter: first-line: placeholder: selection: marker: file: backdrop:
Media feature modifiers
Accessibility and context-aware styles:
<div class="motion-reduce:animation:none">
<nav class="print:display:none"> available: print: motion-safe: motion-reduce: contrast-more: contrast-less: portrait: landscape: forced-colors:
Combining modifiers #
Combined modifiers follow a canonical order enforced with errors that guide you. Multiple
states must be alphabetical (focus:hover: not hover:focus:)
because both generate equivalent CSS -- canonical ordering prevents duplicates.
[media:][ancestor:][...state:][pseudo-element:]class - media - one of
md:,lg:,print:, etc - ancestor - one of
dark:orlight:(likelyrtl:/ltr:in the future) - state - any of
hover:,focus:,disabled:, etc, sorted alphabetically - pseudo-element - one of
before:,after:,placeholder:, etc
<!-- media + ancestor + state -->
<div class="md:dark:hover:opacity:83%">
<!-- media + state + pseudo-element -->
<div class="md:hover:before:opacity:100%">
<!-- multiple states must be alphabetical -->
<button class="focus:hover:outline:2px~solid~blue"> Generated CSS for md:dark:hover:opacity:83%:
@media (width >= 48rem) {
:root.dark .md\:dark\:hover\:opacity\:83\%:hover {
opacity: 83%;
}
}Usage #
npm i -D @fuzdev/fuz_css Import the two required stylesheets:
import '@fuzdev/fuz_css/style.css';
import '@fuzdev/fuz_css/theme.css'; // or bring your own To enable utility classes, use the Vite plugin for a virtual module, or the alternative Gro generator for a static file. Continue reading for the specifics:
Vite plugin #
The Vite plugin extracts classes and generates CSS on-demand. It works with Svelte and plain
HTML/TS/JS out of the box. JSX frameworks (React, Preact, Solid) require the acorn-jsx plugin -- see React and JSX below.
// vite.config.ts
import {defineConfig} from 'vite';
import {sveltekit} from '@sveltejs/kit/vite';
import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.js';
export default defineConfig({
plugins: [sveltekit(), vite_plugin_fuz_css()],
}); Import the virtual module in your entry file, src/routes/+layout.svelte for SvelteKit:
import '@fuzdev/fuz_css/style.css';
import '@fuzdev/fuz_css/theme.css'; // or bring your own
import 'virtual:fuz.css'; // generated on-demand The plugin extracts classes from files as Vite processes them, including from node_modules dependencies. It supports HMR -- changes to classes in your code trigger
automatic CSS updates.
Plugin options
acorn_plugins- required for JSX frameworks, e.g.acorn-jsxinclude_classes- classes to always include (for dynamic patterns that can't be statically extracted)exclude_classes- classes to exclude from outputclass_definitions- custom class definitions to merge with defaults; can define new classes or override existing ones (see composite classes)include_default_definitions- set tofalseto use only your ownclass_definitions, excluding all default token and composite classesclass_interpreters- custom interpreters for dynamic class generation; replaces the default interpreters entirely if provided (most users don't need this)filter_file- custom filter for which files to process. Receives(id: string)and returnsboolean, e.g.(id) => !id.includes('/fixtures/')on_error-'log'or'throw'; defaults to'throw'in CI,'log'otherwiseon_warning-'log','throw', or'ignore'; defaults to'log'cache_dir- cache location; defaults to.fuz/cache/css
TypeScript setup
Add the virtual module declaration, like to vite-env.d.ts:
/// <reference types="vite/client" />
declare module 'virtual:fuz.css' {
const css: string;
export default css;
}Gro generator #
For projects using Gro, create a *.gen.css.ts file anywhere in src/:
// src/routes/fuz.gen.css.ts for SvelteKit, or src/fuz.gen.css.ts, etc.
import {gen_fuz_css} from '@fuzdev/fuz_css/gen_fuz_css.js';
export const gen = gen_fuz_css(); Then import the generated file, in src/routes/+layout.svelte for SvelteKit:
import '@fuzdev/fuz_css/style.css';
import '@fuzdev/fuz_css/theme.css'; // or bring your own
import './fuz.css'; // generated by Gro Generator options
The Gro generator accepts the same options as the Vite plugin, plus additional options for batch processing:
include_stats- include file statistics in output (file counts, cache hits/misses, class counts)project_root- project root directory; defaults toprocess.cwd()concurrency- max concurrent file processing for cache reads and extraction; defaults to 8cache_io_concurrency- max concurrent cache writes and deletes; defaults to 50
Class detection #
The extractor scans your source files and extracts class names using three automatic mechanisms, plus manual hints for edge cases:
1. Direct extraction from class attributes
String literals and expressions in class contexts are extracted directly:
class="..."- static stringsclass={[...]}- array syntax (for clsx-compatible frameworks like Svelte)class={{...}}- object syntax (for clsx-compatible frameworks like Svelte)class={cond ? 'a' : 'b'}- ternary expressionsclass={(cond && 'a') || 'b'}- logical expressionsclass:name- class directives (Svelte)clsx(),cn(),cx(),classNames()- utility function calls
2. Naming convention
Variables ending with class, classes, className, classNames, class_name, or class_names (case-insensitive)
are always extracted, regardless of where they're used:
// extracted because of naming convention
const buttonClasses = 'color_d font_size_lg';
const buttonClass = active ? 'active' : null;
const snake_class = 'snake';
const turtle_class_name = 'turtle'; 3. Usage tracking
Variables used in class attributes are tracked back to their definitions, even if they don't follow the naming convention:
<script>
const styles = 'some-class'; // tracked from class={styles}
const variant = 'other-class'; // tracked from clsx()
</script>
<div class={styles}></div>
<button class={clsx('color_d', variant)}></button> Usage tracking works for variables inside clsx(), arrays, ternaries, and
logical expressions within class attributes. Note that standalone clsx() calls outside
class attributes don't trigger tracking -- use the naming convention for those cases.
4. Manual hints
For dynamically constructed classes that can't be statically analyzed, use the @fuz-classes comment:
// @fuz-classes opacity:50% opacity:75% opacity:100%
const opacity_classes = [50, 75, 100].map((n) => `opacity:${n}%`);
/* @fuz-classes color_a_5 color_b_5 color_c_5 */
const color = get_dynamic_color(); Alternatively, use the include_classes option in your config to the Vite plugin or Gro generator:
vite_plugin_fuz_css({
include_classes: ['opacity:50%', 'opacity:75%', 'opacity:100%'],
}); Use exclude_classes to filter out false positives from extraction. This also suppresses warnings for these classes, even if they were explicitly annotated:
vite_plugin_fuz_css({
exclude_classes: ['some:false:positive'],
});Builtin classes #
fuz_css's main stylesheet provides styles for base HTML elements using style variables, acting as a modern CSS reset that adapts to dark mode. It includes CSS classes that provide common generic functionality -- these are called builtin classes.
.unstyled
Default list (styled):
<ul>
<li>1</li>
<li>2</li>
</ul> - 1
- 2
With .unstyled:
<ul class="unstyled">
<li>a</li>
<li>b</li>
</ul> - a
- b
The .unstyled class lets fuz_css provide solid default element styles with a simple
opt-out:
:where(:is(ul, ol, menu):not(.unstyled)) {
padding-left: var(--space_xl4);
} This strategy supports semantic hooks for theming:
:where(:is(ul, ol, menu):not(.unstyled)) {
padding-left: var(--list_padding_left, var(--space_xl4));
} See the specific docs sections for more about .unstyled.
Other builtin classes
Framework support #
fuz_css is Svelte-first, but the base styles (style.css, theme.css)
work with any framework and plain HTML. The utility class generator has varying detection support:
| framework | detection | notes |
|---|---|---|
| Svelte | full | all patterns including class: directives and array/object syntax |
| plain HTML | full | static class="..." attributes, script variables |
| React / JSX | full | with acorn-jsx plugin - className |
| Preact | full | with acorn-jsx plugin - class |
| Solid | full | with acorn-jsx plugin - class, classList |
| Vue JSX | full | with acorn-jsx plugin - class |
| Vue SFC, Angular, etc. | none | template syntax not parsed; use clsx/cx/cn in JS/TS |
The include_classes plugin config option is an escape hatch for classes that can't be statically detected. Acorn plugins can be added via acorn_plugins for additional syntax support like JSX.
Out of the box, class generation works only with TypeScript/JS, Svelte, and JSX. Angular is not supported; Vue JSX is supported but their recommended SFC format is not. We could revisit this if there's demand.
Svelte-first #
The extractor parses and analyzes the AST to understand Svelte's class syntax. Supported constructs:
- attributes:
class="...",class={[...]},class={{...}}(identifier and string-literal keys),class:name - expressions: logical (
&&,||,??), ternaries, template literals (complete tokens only --`color_a_5 ${base}`extractscolor_a_5, but`color_${hue}_5`cannot be extracted; use@fuz-classesorinclude_classes) - Svelte 5 runes:
$derived()and$derived.by()for class variables - utility calls:
clsx(),cn(),cx(),classNames()with nested arrays, objects, and utility calls - scripts: both
<script>and<script module>, with naming convention and usage tracking
React and JSX #
To enable JSX support for React, Preact, Solid, etc, install acorn-jsx and pass it to the plugin or generator:
npm i -D acorn-jsx Vite plugin
// vite.config.ts
import {defineConfig} from 'vite';
import jsx from 'acorn-jsx';
import {vite_plugin_fuz_css} from '@fuzdev/fuz_css/vite_plugin_fuz_css.js';
export default defineConfig({
plugins: [
vite_plugin_fuz_css({
acorn_plugins: [jsx()],
}),
],
}); Gro generator
// fuz.gen.css.ts
import {gen_fuz_css} from '@fuzdev/fuz_css/gen_fuz_css.js';
import jsx from 'acorn-jsx';
export const gen = gen_fuz_css({
acorn_plugins: [jsx()],
}); Supported JSX patterns:
className="..."andclass="..."- static stringsclassName={clsx(...)}- utility function callsclassName={cond ? "a" : "b"}- ternary and logical expressionsclassList={{active: cond}}- Solid's classList- usage tracking: variables in
className,class, andclassListare tracked back to their definitions (has limitations, room for improvement)
// variable tracking works in JSX too
const styles = 'box hover:shadow_lg';
const Component = () => <div className={styles} />; The acorn_plugins option accepts any Acorn-compatible plugin, so other syntax extensions can be supported the same way.
Custom interpreters #
Interpreters dynamically generate CSS for class names that aren't in the static definitions
(which can be extended via class_definitions or replaced with include_default_definitions: false). The default CSS-literal syntax and modifier support are both implemented as interpreters, which you can extend
or replace.
For advanced use cases, you can define custom interpreters that generate CSS from arbitrary
class name patterns. This is similar to UnoCSS's dynamic rules, which also use regex +
function patterns. An interpreter has a regex pattern and an interpret function that returns CSS (or null to pass):
import type {CssClassDefinitionInterpreter} from '@fuzdev/fuz_css/css_class_generation.js';
// Example: grid-cols-N classes like "grid-cols-4"
// Unlike composites, interpreters can parameterize values
const grid_cols_interpreter: CssClassDefinitionInterpreter = {
pattern: /^grid-cols-(\d+)$/,
interpret: (matched) => {
const n = parseInt(matched[1]!, 10);
if (n < 1 || n > 24) return null;
return `.grid-cols-${n} { grid-template-columns: repeat(${n}, minmax(0, 1fr)); }`;
},
}; This generates grid-cols-1 through grid-cols-24 on-demand --
something that would require 24 separate composite definitions. Note the classes for this
example could also be created as composites with a helper function -- fuz_css uses this
strategy internally to create its token classes in css_class_definitions.ts.
Register with the Vite plugin or Gro generator:
import {css_class_interpreters} from '@fuzdev/fuz_css/css_class_interpreters.js';
vite_plugin_fuz_css({
class_interpreters: [grid_cols_interpreter, ...css_class_interpreters],
}) The interpreter context provides access to class_definitions, css_properties (for validation), and diagnostics (for errors/warnings).
This enables full programmatic control over class-to-CSS generation.
Compared to alternatives #
TailwindCSS and UnoCSS are utility-first frameworks where classes have primacy. fuz_css is semantic-first: utilities complement HTML defaults rather than being the primary styling mechanism.
| TailwindCSS | UnoCSS | fuz_css | |
|---|---|---|---|
| primary syntax | DSL-first | config-first | token DSL + CSS literals |
| multi-property | @apply, plugins | shortcuts | composites |
| arbitrary values | DSL (bg-[#abc]) | any (presets) | CSS syntax (background:#abc) |
| detection | regex | regex | AST (more capable, slower) |
| token source | CSS (@theme) | JS/TS config | TS variables (importable) |
| extensibility | plugins | rules, variants, presets | interpreters |
fuz_css's modifier system is less expressive than TailwindCSS's variants. Missing:
parent/sibling/descendant state (group-hover:, peer-invalid:, has-checked:), arbitrary variants ([&.is-dragging]:), child
selectors (*:), container queries (@md:), data/ARIA variants, and
more. When you need these patterns, fuz_css currently expects you to use rulesets or <style> tags, but the API is still a work in progress, and a more powerful and
potentially more TailwindCSS-aligned system is on the table.
For extensibility, all three frameworks allow custom class-to-CSS mappings. UnoCSS's dynamic rules use regex + function patterns similar to fuz_css interpreters, plus separate variants for modifiers. TailwindCSS uses JS plugins and UnoCSS has the more mature extensibility story; fuz_css offers comparable power with interpreters but it's still evolving -- feedback is welcome!
fuz_css fits best when you prefer semantic HTML with styled defaults. Design tokens are
defined in TypeScript, naturally adapt to dark mode, and can be imported in TS for typesafe
runtime access. The tradeoffs include a more limited DSL and more verbose literal syntax,
which nudges you toward <style> tags, tokens when appropriate, or composites
for repeated patterns.
fuz_css is still early in development. Your input is welcome in the discussions!
colors #
Color semantics #
fuz_css provides a palette of color and hue variables designed to support concise authoring in light and dark modes, as well as straightforward theming by both developers and end-users at runtime. The colors have more semantics than just plain values, so they automatically adapt to dark mode and custom themes, at the cost of having different values depending on color scheme and theme.
Adapting colors to dark mode
A color's subjective appearance depends on the context in which it's viewed, especially the surrounding colors and values. fuz_css's semantic colors are designed to work across color schemes, so each fuz_css color variable has two values, one for light and one for dark mode. The exceptions are the lightest (1) and darkest (9) variants, although this may change if it yields better results.
Custom themes
Instead of "blue" and "red", colors are named with letters like "a" and "b", so you can change the primary "a" from blue to any color in a theme without breaking the name-to-color correspondence everywhere. This also flexibly handles more colors and cases than using names like "primary", and although it takes some learning, it's a simple pattern to remember. ("primary" and its ilk require learning too!)
A downside of this approach is that changing a color like the primary "a" affects the many places it's used. Sometimes you may want to change the color of a specific element or state, not all the things. In those cases, use plain CSS and optionally fuz_css variables. Compared to most libraries, fuz_css provides fewer handles for granular color customizations, but the benefits include consistency, efficiency, DRY authoring, and ease of app-wide theming.
Caveats #
For performance reasons, fuz_css does not currently have an extensive set of variants, like
specialized states for elements or color values like "blue". Each of the 7 hues has 9 HSL
color values (e.g. hsl(120 55% 36%)) and 9 HSL component values (e.g. 120 55% 36%, useful to efficiently apply custom alpha), handling most cases, and
the base colors can be customized with platform APIs like the color-mix CSS function.
Variants will be expanded when fuz_css includes a Vite plugin or other build tooling for optimization. A downside of removing unused styles is that they won't be available to your end-users at runtime. We'll probably end up with an interpreted language like Tailwind's just-in-time compiler.
Hue variables #
Hue variables contain a single hue number. Each color variable combines a hue variable with hardcoded saturation and lightness values for light and dark modes.
Hue variables therefore provide a single source of truth that's easy to theme, but to achieve pleasing results, setting the hue alone is not always sufficient. Custom colors will often require you to set per-variable saturation and lightness values.
Hue variables are also useful to construct custom colors not covered by the color variables.
For example, fuz_css's base stylesheet uses hue_a for the semi-transparent ::selection. (try selecting some text - same hue!)
Unlike the color variables, the hue variables are the same in both light and dark modes.
- NaNprimary
- NaNsuccess
- NaNerror/danger
- NaNsecondary/accent
- NaNtertiary/highlight
- NaNquaternary/muted
- NaNquinary/decorative
- NaNsenary/caution
- NaNseptenary/info
- NaNoctonary/flourish
Color variables #
There are 9 variables per color, numbered 1 to 9, lightest to darkest. The 5th variable of each color is used as the base for things like buttons.
Note that these values differ between light and dark modes! See the discussion above for why.
These colors were eyeballed by a programmer, and will change :]
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
- rgb()
buttons #
The <button> element is styled by default without adding classes. Classes
like .selected and .plain and .color_a modify the base style.
Buttons have a .selected state that can be used for various UI purposes, like
showing a selected item in a menu or a styling button's aria-pressed state.
Instead of having two distinct styles of buttons with outlined and filled variants, fuz_css
makes outlined buttons the default, and selected buttons are filled. There's also the .deselectable modifier class for buttons that remain clickable when selected. Themes
can customize this behavior.
<button>a button</button> Colorful buttons #
<button class="color_a"> <button class="color_b"> <button class="color_c"> <button class="color_d"> <button class="color_e"> <button class="color_f"> <button class="color_g"> <button class="color_h"> <button class="color_i"> <button class="color_j"> With disabled attribute #
<button disabled>
:|
</button> With .selected #
.selected<button class="selected">...</button> .selected buttons with .deselectable continue to be clickable when selected:
<button class="selected deselectable">
...
</button>With .plain and .icon_button #
.plain and .icon_button<button class="plain">
+
</button> <button class="icon_button">
+
</button> <button class="plain icon_button">
+
</button> .selected variants
<button class="plain selected">
+
</button> <button class="icon_button selected">
+
</button> <button class="plain icon_button selected">
+
</button> .selected and .deselectable variants
<button class="plain selected deselectable">
+
</button> <button class="icon_button selected deselectable">
+
</button> <button class="plain icon_button selected deselectable">
+
</button> elements #
fuz_css applies default styles to semantic HTML elements in its 🗎 reset stylesheet. The styles use variables and include appropriate spacing, so plain HTML gets
user-friendly styling and theme integration automatically. The defaults are low specificity using :where so they're easy to override, and you can opt out by adding .unstyled to an element.
#
Paragraph elements are unstyled except for spacing. Divs are totally unstyled.
p
p
p
p
This paragraph has no bottom margin because default spacing is omitted for the :last-child of all otherwise-spaced elements, streamlining the common case. This has some unfortunate edge cases
that can usually by solved by adding .mb_lg. Coupling markup structure to styles
like this may be something we change, feedback is
welcome.
#
a link with .selected
a link with .unstyled
#
code
code with .unstyled
#
a pre is
preformatted
text#
Click this summary to see the rest of the details
The children of the details excluding the summary.
<details>
<summary>
Click this <code>summary</code>
to see the rest of the <code>details</code>
</summary>
<p>The children of the <code>details</code> excluding the <code>summary</code>.</p>
<Code code={'...'} />
</details>details and summary with .unstyled
unstyled details content#
blockquote
blockquote with .unstyled#
.unstyled#
#
<header>header</header> #
<footer>footer</footer> #
<section>section</section> Sections have a bottom margin, except for the last in the list.
ul #
ul- a
- b
- see
ul with .unstyled
- a
- b
- see
ol #
ol- one
- two
- etc
ol with .unstyled
- one
- two
- etc
menu #
menumenu with .unstyled
#
<table>
<thead>
<tr>
<th>th</th>
<th>th</th>
<th>th</th>
</tr>
</thead>
<tbody>
<tr><td>td</td><td>td</td><td>td</td></tr>
<tr><td>td</td><td>td</td><td>td</td></tr>
<tr><td>td</td><td>td</td><td>td</td></tr>
</tbody>
</table> | th | th | th |
|---|---|---|
| td | td | td |
| td | td | td |
| td | td | td |
<table class="width:100%">
...
</table> | th | th | th |
|---|---|---|
| td | td | td |
| td | td | td |
| td | td | td |
TODO more!
forms #
Form elements have basic default styles that can be omitted with .unstyled.
#
<form>
<fieldset>
<legend>
a <MdnLink path="Web/HTML/Element/legend" />
</legend>
<label>
<div class="title">
username
</div>
<input
bind:value={username}
placeholder=">"
/>
</label>
...
</fieldset>
...
</form> form with range input #
form with range inputform with checkboxes #
form with checkboxes<label class="row"> with .disabled as needed: <label class="row disabled">form with radio inputs #
form with radio inputstypography #
h1
h2
h3
h4
h5
h6
paragraphs
paragraphs
paragraphs
p with some small text
p sub p sup p
show code
Font families #
Font sizes #
Font weights #
Font weight values can be any integer from 1 to 1000.
There are no variables for font-weight but there are utility classes.
Text colors #
Line heights #
Icon sizes #
--font_size_ variables, --icon_ variables are in px not rem, so they're insensitive to browser font size18px32px48px80px128px192px256pxborders #
Border shades #
Border colors #
Border widths #
Outlines #
Each border utility class has a corresponding outline variant using the same border variables
(like outline_color_b, outline_width_4, and outline-style:solid), and there are also two special outline variables:
Border radius #
Border radius percentages #
Interpreted utility classes, 0 to 100 (%).
Border radius corners #
Border radius corner percentages #
Interpreted utility classes, 0 to 100 (%).
shading #
fuz_css is designed around two simplistic models of light, one for dark mode and one for light mode, mapping to the web platform's color-scheme. The goal is easy authoring with simple and consistent rules for arbitrary compositions and states. Each theme can choose to implement either light mode or dark mode or both.
Light mode's starting point is plain white documents (like paper) where we can think of UI construction as assembling elements that contrast against the white background, replacing some of the white blankness with darkened values/color/shape. In other words, we start with full lightness and subtract light to make visuals. Black shadows on the white background make natural sense, and white glows against a white background are invisible.
In contrast, dark mode's starting point is a lightless void where we add light. We add elements which emanate light. I think of videogames and virtual/augmented/actual reality. Black shadows are invisible against a black background, and white glows make natural sense against a black background.
This distinction leads to complication. For example, applying a black shadow to an element has a particular visual impact on the typical light mode page, but viewed in dark mode, it would disappear completely against a black background.
fuz_css provides APIs that simplify or hide this complexity. For example, the lighten and darken variables are the same in light and dark modes, but fg and bg are equivalent values that swap places in dark mode. Thus bg and fg are called color-scheme-aware, and lighten and darken are color-scheme-agnostic. (maybe you can think of better terminology? I
like the word "adaptive" but idk) The colors docs elaborate more on this point
and the shadows docs implement more of the idea.
Opacity is used to enable arbitrary stacking that visually inherits its context. Not all cases are properly handled yet, and some choices are made for performance reasons, like avoiding opacity on text. (assuming this is still a thing?)
Shades and highlights #
darken and lighten #
darken and lightenbg and fg #
bg and fgIn light mode, bg is the same as lighten and fg is
the same as darken. In dark mode, they're swapped.
tip: Try between light
and dark to see how bg and fg change, while darken and lighten don't change but do appear significantly
different because of the context.
Stacking transparency #
Many styles are designed to stack, so things can appear in different contexts while retaining relative color value distinctiveness ("color value" as in darkness-lightness). Internally this uses simple transparency instead of complex selectors or other structure.
<div class="fg_1 p_sm">
<div class="fg_1 p_sm">
<div class="fg_1 p_sm">
<div class="fg_1 p_sm">
<div class="bg p_sm">
...
</div>
</div>
</div>
</div> these shades use opacity, but notice how contrast changes with depth, creating limitations
This adds some complexity and performance costs, and it's currently incomplete, but so far it feels like an elegant solution with many unfinished details, and I plan to continue integrating the idea in more places while considering alternative designs. However alpha transparency has multiple costs, so I'm trying to be mindful to not use alpha for text and other cases that are more performance-sensitive, but we may need to change this behavior for the base cases, or include performance themes.
Opacity #
Interpreted utility classes, 0 to 100 (%).
shadows #
fuz_css's shadows build on the light model discussed in the shading docs.
Shadow #
Shadows darken in light mode and lighten in dark mode.
shadow_alpha_ Highlight #
Hightlights lighten in light mode and darken in dark mode.
shadow_alpha_ Glow #
Glows lighten in both light and dark mode.
shadow_alpha_ Shroud #
Shrouds darken in both light and dark mode.
shadow_alpha_ Colorful shadows #
These are darker in light mode than in dark mode.
shadow_alpha_ layout #
Space variables #
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
- =
Space variants are used in classes like .p_md for padding, margin, other forms of spacing like gap, positioning, dimensions, etc.
Distance variables #
- =
- =
- =
- =
- =
Distance variants have classes like .width_atmost_sm and .width_atleast_md.