Design system · MUI replacement
One vocabulary. HTML, CSS and a sprite. No framework.
This site is the reference implementation of the QFlow design system as plain HTML and CSS. Every element in the catalog is rendered here from tokens up, so it can be lifted into React, a server-rendered page or anything else that emits markup. It does not depend on Material UI and is not meant to run beside it.
Principles
| Tokens or nothing |
No literal color, size, radius, shadow or duration exists outside tokens.css. Dark
mode is a pure token override. The geometry the classic skin needs beyond tokens (chip tabs, 20px
menu rows, the dialog frame) lives in css/classic/ rather than in attribute
overrides.
|
|---|---|
| Three tiers, strict dependencies | A primitive depends on tokens only and owns one concern. A component is composed of primitives. A composite assembles components into a whole interaction and may be domain-aware. Nothing reaches down more than one tier. |
| State is an attribute, not a class |
Hover, selected, pressed, expanded, invalid and disabled are expressed with aria-*,
data-state and native pseudo-classes. Framework bindings set attributes; they never
compute class strings.
|
| Configuration is a data attribute |
Every axis in the catalog (variant, color, size,
severity…) is data-{axis}="{value}" on the block. The React prop is the
attribute, one to one.
|
| Native first |
Dialogs are <dialog>, menus and popovers use the popover
attribute, accordions are <details>, toggles are real inputs. Behavior that the
platform provides is not re-implemented.
|
| Icons are a sprite |
One icons.svg of <symbol>s, referenced with
<use>. Adding an icon is a line in icons.txt; nothing is bundled
per icon and no icon library ships to the client.
|
The markup contract
Class names follow qf-{block} and qf-{block}__{element}. Modifiers are never
classes. This example is a complete, valid Button and shows every kind of hook the system uses.
| Hook | Form | Meaning |
|---|---|---|
| Block | class="qf-button" |
Which element of the catalog this is. Exactly one per element. |
| Element | class="qf-button__label" |
A named child that the block styles. Only used where the child has no meaning on its own. |
| Axis | data-variant="outlined" |
Configuration. Enumerated values only; documented per element. |
| Flag | data-loading, data-block |
Boolean axis. Presence is true. |
| State | aria-pressed="true", :disabled, data-state="open" |
Runtime state. Prefer the ARIA attribute the element already needs; data-state only
when none exists.
|
| Child element | <svg class="qf-icon"><use href="…#id"/></svg> |
Composition. A component contains primitives by nesting their markup; the parent only adjusts size and color. |
Consuming it
The system has one skin, classic: one complete stylesheet set. Link css/classic/index.css
and copy sprites/icons.svg and fonts/ beside it. That is the entire runtime.
Set data-theme="dark" on <html> for the dark theme. Pages also carry
data-skin="classic" on <html> for scripts; no rule depends on it.
<link rel="stylesheet" href="design/css/classic/index.css"> <button class="qf-button" data-variant="contained" data-color="primary"> <svg class="qf-icon"><use href="design/sprites/icons.svg#plus"/></svg> New task </button>
type ButtonProps = {
variant?: 'contained' | 'outlined' | 'text' | 'ghost' | 'soft';
color?: 'primary' | 'info' | 'success' | 'warning' | 'error';
size?: 'xs' | 'sm' | 'md';
iconOnly?: boolean;
loading?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export const Button = ({ variant, color, size, iconOnly, loading, ...rest }: ButtonProps) => (
<button
className="qf-button"
data-variant={variant}
data-color={color}
data-size={size}
data-icon-only={iconOnly || undefined}
data-loading={loading || undefined}
{...rest}
/>
);
export const Icon = ({ name, size, color }: IconProps) => (
<svg className="qf-icon" data-size={size} data-color={color} aria-hidden="true">
<use href={`${SPRITE_URL}#${name}`} />
</svg>
);
// Inline styles keep working; the cascade resolves the variable.
<div style={{ color: 'var(--qf-color-primary-500)', padding: 'var(--qf-space-4)' }} />
// A generated tokens.ts can expose the same names typed:
export const color = { primary: { 500: 'var(--qf-color-primary-500)' /* … */ } } as const;
Files
| Path | What it is |
|---|---|
css/classic/ |
The one complete stylesheet set (the classic skin). No rule has a [data-skin]
selector; every file stays at 600 lines or fewer.
|
css/classic/index.css |
Imports the set in order. The only file a consumer links. |
css/classic/tokens.css |
Every value. Light on :root, dark as [data-theme='dark']; hex only in
--qf-palette-*.
|
css/classic/base.css |
Font faces, reset, focus ring, scrollbars, reduced-motion. |
css/classic/primitives/ |
P-01 … P-14: layout.css and behavior.css. |
css/classic/components/ |
C-01 … C-29, grouped by catalog section: actions, content, inputs, navigation, overlays, utility. |
css/classic/composites/ |
The app shell, X-01 OverlaySystem and X-02 … X-11: data, documents, feedback. |
sprites/icons.svg |
Generated sprite. icons.txt is the curated list; build-sprite.mjs
regenerates from Tabler.
|
fonts/ |
Avenir Next (300–700, italic) and Twemoji Mozilla. |
css/site/, js/docs.js, js/trace.js |
This site only. Not part of the system. |
serve.mjs |
bun run dev serves the site on port 5190 with live reload and no dependencies. |
Status
Visual contract complete for all four layers. Behavioral primitives (VirtualScroller, DragSource, useCombobox) have no visual output and are documented as contracts on the primitives page. Composites show geometry and states; their data behavior lands with the framework binding.
Accessibility: each element's WCAG 2.2 AA claim is on Test status; see also the accessibility statement and the conformance report.