Quill docs
Styled components (qquill-design)
qquill-design is the styled component library — the layer one level above the headless state machines. For each component it takes a headless qquill_ui State (which already owns the ARIA / data-* contract), maps it onto a qquill_view::Node with variant classes and CSS-variable-based colors, and returns a Styled: the rendered node plus a companion qquill_style::StyleBlock carrying that component's CSS plus an optional IslandHint for Phase-3 island registration. Accessibility is inherited from qquill_ui via Component::apply_attrs, so the design layer never re-derives it — it only adds presentation. Every color is a var(--q-color-…) reference, so flipping data-q-theme on <html> restyles every component at once with no reflow. Zero third-party dependencies.
The three-step model#
headless State (qquill_ui) design layer (qquill_design)
Button { pressed, disabled } ─▶ 1. take the headless State (owns ARIA/data)
attrs(): aria-pressed, 2. map -> Node + variant classes + var(--q-…)
aria-disabled, data-state, 3. return Styled { node, StyleBlock, IslandHint? }
data-q-ui │
▼
node: <button class="qq-btn qq-btn--md …"
data-q-ui="button" aria-pressed="true">
style: .qq-btn{…} .qq-btn--brand.qq-btn--solid{…}
island: IslandHint{ kind:"button", OnInteraction }Because ARIA/data attributes are inherited (the design layer calls self.ui.apply_attrs(node)), they are never re-derived. The headless Button stamps aria-pressed / aria-disabled / data-state / data-q-ui; the styled Button only adds type="button" and the variant class list on top.
Styled: the return type of every component#
Every design component's render() returns a Styled — a Node tree, its CSS, and an optional island hint. You collect each component's style block into one sheet for the <head> and drop each node into the page tree.
- node()
- Borrow the rendered Node.
- style()
- Borrow the companion StyleBlock (call .to_css() to get the CSS text).
- island()
- The IslandHint, if the component is interactive (Option<IslandHint>).
- is_interactive()
- true when an island hint is present — i.e. the component would hydrate in Phase 3.
- into_node()
- Consume into just the Node (drop into a parent tree).
- into_parts()
- Consume into (Node, StyleBlock) — the common case when a page assembles its tree and its sheet separately.
Styled::new(node, style) builds a static (non-interactive) component; Styled::interactive(node, style, hint) builds one carrying an island hint.
IslandHint: the Phase-3 seam#
Interactive components carry an IslandHint — a flag, not an implementation. This crate ships no JS runtime; the hint is the seam a Phase-3 islands build would read to decide which subtrees to hydrate and when. kind is the headless primitive's data-q-ui token (e.g. "button", "menu") — what a Phase-3 island registry keys on. trigger is the lazy hydration trigger.
| IslandTrigger | Wire token (data-q-trigger) | Meaning |
|---|---|---|
| OnLoad | load | Hydrate as soon as the runtime boots. |
| OnInteraction (default) | interaction | Hydrate on first interaction (focus / click / keydown) — the cheapest default for most controls. |
| OnVisible | visible | Hydrate when scrolled into view. |
| OnIdle | idle | Hydrate when the browser is idle. |
Storing the hint means a page can inventory its interactive components today — for a manifest — without any runtime existing yet. A button, for example, reports IslandHint { kind: "button", trigger: OnInteraction }.
Variant axes are const lookup tables#
Variants are tiny closed enums whose CSS-class suffix and token references resolve through a const-evaluable match -> &'static str the compiler folds — there is no runtime string concatenation to build a class name. There are six axes. The first three live in the Variants struct (size / tone / variant); effect and radius are additive modifiers set on the component builder; logos use Mark separately.
| Axis | Type | Values | Default |
|---|---|---|---|
| Size | Size | Sm, Md, Lg | Md |
| Tone | Tone | Brand, Neutral, Danger | Brand |
| Variant (fill) | Variant | Solid, Soft, Outline, Ghost | Solid |
| Effect (surface) | Effect | Flat, Glass, Neumorphic, Gradient, Elevated | Flat |
| Radius (corners) | Radius | None, Sm, Md, Lg, Xl, Full | Md |
- Size
- Spacing + type-scale ramp. class() -> "sm"/"md"/"lg"; also resolves pad_space, pad_x_space, and font_size token names.
- Tone
- Semantic color role — selects WHICH --q-color-* family is pulled (fill_token, accent_token, on_fill_token). The hex lives in the theme and differs light/dark, so a tone restyles on theme switch with zero reflow.
- Variant
- Fill treatment. Solid = filled bg + contrast text; Soft = tinted bg + tone text; Outline = transparent + tone border + tone text; Ghost = transparent + tone text only. is_filled() is true only for Solid; has_border() true only for Outline.
- Effect
- Orthogonal surface treatment layered on top of Variant without touching tone/fill semantics. class() -> "flat"/"glass"/"neu"/"gradient"/"elevated"; built from --q-effect-*/--q-shadow-* tokens, so it restyles per mode. is_decorated() is true for everything but Flat.
- Radius
- Corner-radius scale mapping onto --q-radius-* (none/sm/md/lg/xl/full); Full is a pill/circle clamp. The class suffix and token name coincide.
Variants::class_list(base) builds the BEM-style class list. Each suffix comes from a const table; the only allocation is the joined string itself, built once per render rather than per axis.
// Variants::default() is md / brand / solid.
let v = Variants::new().size(Size::Lg).tone(Tone::Danger).variant(Variant::Outline);
v.class_list("qq-btn");
// => "qq-btn qq-btn--lg qq-btn--danger qq-btn--outline"
//
// Every (size, tone, variant) triple builds a well-formed list:
// "qq-btn qq-btn--{size} qq-btn--{tone} qq-btn--{variant}"Worked example: a styled Button#
Button::action(label) builds a plain action button with default variants; Button::toggle(label, on) builds a toggle. The builder is fluent: .variants(...), .effect(...), .radius(...), .disabled(...). render() produces the Styled. The class list leads with the variant axes (base/size/tone/variant) followed by the additive effect and radius modifiers — which always emit a modifier (flat / r-md by default) so the companion CSS rule always has a selector to bind to.
Request
use qquill_design::{Button, Variants, Tone, Variant};
use qquill_view::render;
let styled = Button::action("Save")
.variants(Variants::new().tone(Tone::Brand).variant(Variant::Solid))
.render();
let html = render(styled.node());Response
// styled.node() (note: defaults size=Md, effect=Flat, radius=Md):
// class = "qq-btn qq-btn--md qq-btn--brand qq-btn--solid qq-btn--flat qq-btn--r-md"
// type = "button"
// data-q-ui="button" <- INHERITED from the headless layer
//
// styled.style().to_css() contains:
// var(--q-color-brand) var(--q-color-on-brand)
// var(--q-radius-md) var(--q-font-sans)
// selector .qq-btn--brand.qq-btn--solid (no hardcoded hex)
//
// styled.is_interactive() == true
// styled.island() == Some(IslandHint { kind: "button", trigger: OnInteraction })Output
<button class="qq-btn qq-btn--md qq-btn--brand qq-btn--solid qq-btn--flat qq-btn--r-md" type="button" data-q-ui="button" data-state="idle">Save</button>A disabled toggle proves inheritance: Button::toggle("Mute", true).disabled(true).render() emits data-q-ui="button", aria-pressed="true", aria-disabled="true", data-state="on", and a bare disabled — all contributed by the headless machine, none re-derived by the design layer.
Dark / light aware, zero reflow#
Components never hardcode a hex color. Every color is a var(--q-color-…) reference resolved through the qquill_theme tokens. The CSS helper functions (css::color, css::space, css::radius, css::font, css::duration, css::shadow, css::effect, css::tone_color) all emit var(--q-…) strings. Flipping the data-q-theme attribute on <html> re-points every reference at once, so a component restyles on theme switch with no reflow. The test suite asserts that no brand hex (e.g. #4f46e5) ever leaks into component CSS.
The component set#
qquill-design ships 30+ styled components, each wrapping its headless counterpart. The public surface includes: Accordion (with Section), Alert, Avatar, Badge, Breadcrumb (with Crumb), Button, Card, Checkbox, Combobox (with ComboOption), CommandPalette (with Command), Dialog, Divider, Drawer, Field, List (with ListItem), Logo (with Mark), Menu (with MenuItem), Navbar (with NavLink), NumberInput, PageNav (with Target), Pagination, Popover, Progress, Radio, Select (with Option), Skeleton, Slider, Spinner, Stat (with Trend), Stepper, SwitchGroup, Table (with HeaderCell), Tabs, Textarea, Toaster, and Tooltip. Side (for Drawer) and Severity (for Toaster) are re-exported from the headless layer.