P · Primitives
One concern each
A primitive depends on nothing but tokens, owns exactly one concern and is never domain-aware. Eleven have a visual contract and are shown here; three (VirtualScroller, DragSource, useCombobox) are behavioral kernels whose output is consumed by List, Table and Select.
The layout host: every rectangle in the app. Absorbs the two thirds of inline styles that are pure layout. A Box has no color of its own.
| Axis | Values |
|---|---|
data-display |
flex · inline-flex · grid · inline · none |
data-direction |
row · column · row-reverse |
data-gap · data-p · data-px · data-py |
0 · 1 · 2 · 3 · 4 · 6 · 8 (token scale) |
data-align · data-justify |
start · center · end · stretch · baseline / start · center · end · between · around |
data-wrap · data-flex · data-w · data-h · data-overflow · data-position · data-cols |
wrap / 1 · auto · none / full / hidden · auto / relative · absolute · sticky / 2 · 3 · 4 · auto |
All typography, headings included. No attributes gives sm / regular / ink. Layout-only styling
(overflow, whitespace) is the only thing that goes in style.
| Axis | Values |
|---|---|
data-size |
xs · sm · md · lg · xl · h1 · h2 · h3 · h4 · h5 · h6 · overline |
data-weight |
light · regular · medium · semibold · bold · heavy |
data-tone |
secondary · muted · disabled · link · inverse |
data-color |
primary · info · success · warning · error · purple · electric-blue · pink · yellow · brown |
data-align · data-truncate · data-mono |
center · end / (1) · 2 · 3 lines / monospace |
Default: 14px, weight 500, ink.
Secondary tone for supporting copy.
Muted tone for metadata and captions.
Disabled tone.
Link tone.
Success color, medium weight.
Error color, medium weight.
Purple accent.
Truncated: this sentence is far too long for the width of its container and will be clipped with an ellipsis.
Two-line clamp: this sentence is far too long for the width of its container and will be clipped after the second line, with an ellipsis at the end of the second line.
monospace · 4f3c-91ab-…
One SVG glyph from the sprite. Strictly non-interactive (pointer-events: none): wrap it in a
Pressable or Button to make it clickable. Default 20px, 24px on mobile. Never size the inner glyph; size the
wrapper.
Glyph families. Tabler (sprites/icons.svg) everywhere. Editor
exception: the QEditor editors use Fluent UI System Icons, Regular style only
(sprites/office-icons.svg), inside .qf-office and qf-office__*
elements and in the office catalog sections. The two families never mix, and office symbols carry
stroke="none" so this primitive's stroke never outlines them. Both rules are gated; see the
README's "Icons" section.
| Axis | Values |
|---|---|
data-size |
inherit · xs (14) · sm (16) · md (20) · lg (24) · xl (32) |
data-color |
inherit · primary · info · muted · success · warning · error · purple · electric-blue · pink · yellow · brown · inverse |
data-spin · data-pulse |
loading animations |
A bounded visual container: background, border, radius, elevation and the shared hover/selected pattern that cards, list rows and kanban cards all use.
| Axis | Values |
|---|---|
data-tone |
(paper) · raised · sunken · primary · info · success · warning · error · inverse |
data-radius |
none · sm · md · lg · xl |
data-elevation |
xs · sm · md · lg · xl · 2xl |
data-bordered · data-interactive |
1px token border / hover + selected + focus states |
aria-selected · data-state |
true / hover · selected |
The single interaction target: focus ring, keyboard activation, hover/press/selected state, disabled
semantics. Button, Chip, Tab, Menu.Item and list rows are all Pressables. Use a real
<button> or <a>.
| Axis | Values |
|---|---|
data-shape |
circle |
data-icon-only · data-size |
square hit target / xs · sm · md |
data-block |
full width, start-aligned (list rows) |
aria-pressed · aria-selected · :disabled |
state |
Overlay positioning, scrim, focus containment and dismissal: the one primitive behind every floating
surface. In static HTML the platform provides most of it: <dialog> for modal/drawer,
popover for popover/menu/tooltip. The class exists for hosts that need manual stacking.
| Axis | Values |
|---|---|
data-kind |
modal · drawer · popover · tooltip · toast |
data-placement |
left · right · bottom (drawer) |
data-state · data-depth |
open · closed / 0 · 1 · 2 · 3 (stack offset, +10 z each) |
Enter/exit presets driven by data-state, plus keyframe classes for elements that mount already
open. Honors prefers-reduced-motion globally.
| Axis | Values |
|---|---|
data-preset |
fade · scale · slide-up · slide-down · slide-left · slide-right · collapse |
data-state · data-speed |
open · closed / fast · slow |
.qf-enter-* |
one-shot mount animations with the same names |
A scroll container whose edge shadows appear only when there is more content in that direction. Pure CSS:
the shadows are background layers with background-attachment: local, so no scroll listener is
needed. Replaces ScrollShadows.
| Axis | Values |
|---|---|
data-axis |
y (default) · x |
data-hide-scrollbar |
hide the bar, keep the shadows |
-
Row 1
-
Row 2
-
Row 3
-
Row 4
-
Row 5
-
Row 6
-
Row 7
-
Row 8
| Axis | Values |
|---|---|
data-orientation |
horizontal (default) · vertical |
data-tone · data-inset · data-label |
subtle · strong / inset margins / labeled rule |
Right
| Axis | Values |
|---|---|
data-shape |
text · rect · circle |
style width/height |
geometry only; always aria-hidden |
The bare focusable form element with no chrome. Field (C-09) supplies the label, border and helper shell;
Control is the <input>, <textarea> or
<select> inside it. Shown here without a Field to make the point.
| Axis | Values |
|---|---|
type · placeholder · inputmode · autocomplete |
native attributes pass through |
data-align · data-mono |
end (numeric) / monospace |
:disabled · [readonly] |
native state |
These own behavior, not pixels. Their visual output is whatever the consuming component renders; the
contract below is what the static layer already provides for them. P-15 to P-20 serve the QEditor
office surfaces (office mockups); their demo
behavior lives in js/office.js.
| Primitive | Contract | Static hooks provided |
|---|---|---|
| VirtualScroller P-11 · #438 |
Windowed rendering of long homogeneous collections. itemCount,
itemSize (fixed or measured), overscan, scrollToIndex.
|
Nothing visual. Renders inside a .qf-scroll; rows are ordinary
.qf-list__item / <tr> positioned by transform.
|
| DragSource P-12 · #439 |
Pointer/keyboard drag kernel: move, sort, resize. Ships first as an adapter over the retained
@dnd-kit.
|
data-state="dragging" on the source, data-state="over" on the target,
data-state="overlay" on the clone, and the handle classes .qf-list__handle,
.qf-resizable__handle, .qf-table__resize. In the nav's organize mode
(composites/nav-organize.css): .qf-nav__item or .qf-nav__row
takes data-state="dragging" and "overlay" and is its own handle, a
.qf-nav__group takes "dragging" as a whole,
.qf-nav__row or .qf-nav__subitems takes data-state="over",
and .qf-nav__drop-indicator marks the drop line. Every dragged outcome also has
a keyboard and menu path (WCAG 2.5.7). The rest of the nav tree's hooks are below.
|
| useCombobox P-14 · #441 · gate for Select |
Headless combobox/listbox: virtual focus, typeahead, ARIA wiring. Must merge before any Select variant is touched. |
aria-expanded on the trigger, role="listbox" +
.qf-listbox__option[data-state="active"] for virtual focus,
aria-selected for selection, aria-multiselectable for multi.
|
| RovingFocus P-15 · office |
One tab stop per composite widget; arrow keys move, Home/End jump, optional wrap, 2-D mode for grids; skips hidden items and yields Left/Right to a text input with a caret (QEditor spec §4.1). |
[data-rove] on the container (a role="toolbar", tablist,
listbox or grid); tabindex="0" on the current item and
-1 on the rest. Ctrl+←/→ jumps to the next .qf-ribbon__group.
Demo: js/office.js.
|
| KeyTips P-16 · office |
Access-key layers: Alt+Windows (Windows) or Ctrl+Option (macOS) shows badges on File and the tabs; a letter selects a tab and shows that panel's badges; typing filters, Esc steps back one layer. |
data-keytip="H" on the target, rendered as .qf-keytip (C-31,
aria-hidden; data-state="dimmed" for non-matching tips) and mirrored as
aria-keyshortcuts on the control.
|
| TextInputProxy P-17 · office |
The element that holds focus while editing a canvas: EditContext where available, else a hidden
<textarea> moved to the caret for IME; routes keys, composition and clipboard
to the engine (spec §4.5).
|
Nothing visual. Carries aria-label ("Editing cell G13", "Slide 3 title placeholder") and
aria-describedby → the mirror node; the mockups stand in with a
role="textbox" on the edited content.
|
| AccessibleMirror P-18 · office |
Off-screen semantic DOM for canvas content near the viewport and caret: headings, paragraphs, tables, lists, slide groups, grid cells, PDF text and annotations, kept in sync with the engine. |
role="document" with aria-roledescription="page" per page,
role="grid" with aria-rowcount/aria-colcount for sheets,
role="group" aria-roledescription="slide" for slides. The mockups' canvases are
written in this form.
|
| LiveAnnouncer P-19 · office |
One polite and one assertive region per app, with de-duplication and a one-second coalescing window ("Bold on", "3 of 27", "Connection lost. Your changes are kept."). |
.qf-office-app__live[role="status"] and .qf-office-app__live[role="alert"],
visually hidden, inside .qf-office-app.
|
| AdaptiveCollapse P-20 · office |
ResizeObserver-driven fitting of the ribbon (Compact and Full alike), status bar and tab row: as the width shrinks it sheds labels (icon-only, the name stays in aria-label and the tooltip), narrows wide comboboxes, moves controls into the trailing ⋯ menu by priority (last group first), then folds whole groups into dropdowns. Groups never overlap or clip; horizontal scrolling is the last resort. |
data-priority="1..4" on controls, data-collapsed="label small narrow hidden group" tokens
written by the primitive, .qf-ribbon__more and .qf-ribbon__collapsed-group
as the overflow targets; the container queries on .qf-office-app cover the widths
of spec §1.13.
|
Nav tree hooks
The sidebar tree in the shell composite (composites/nav-tree.css and
nav-organize.css). The shell composite shows a nested tree with its row menus.
| Hook | Item | Markup and meaning |
|---|---|---|
.qf-nav__subitems + style="--qf-nav-depth: N" |
D-1 | A section of the tree. N is 1 under an entry and one more per group level; everything inside indents by N × --qf-nav-indent-step. It holds rows, groups, bare items, skeletons or an empty line. |
.qf-nav__row in a section |
D-1, D-3 | A search: a.qf-nav__item (aria-keyshortcuts="Shift+F10") and its row menu, a .qf-nav__action with aria-haspopup="menu", aria-controls and aria-expanded. The trigger keeps its hover state while expanded. |
.qf-nav__group |
D-2 | role="group", labelled by its item. Holds a .qf-nav__row whose button.qf-nav__item starts with a .qf-nav__chevron icon and spans the twisty and label columns, then the group's .qf-nav__subitems. Outside organize mode the button toggles the group (aria-expanded, aria-controls the subitems) and the chevron follows it; the layout follows the structure, not the attribute. |
| Right-click, Shift+F10, context-menu key | D-3 | Behaviour, in the binding: open the row's .qf-nav__action menu. js/docs.js demonstrates it on the shell composite. |
data-hidden, .qf-nav__marker |
D-4 | On a .qf-nav__row, .qf-nav__group or bare .qf-nav__item. Not shown outside organize mode (the app leaves it out). In organize mode the item is italic in --qf-nav-hidden-ink and ends with <span class="qf-nav__marker">(Hidden)</span>. |
.qf-nav__add |
D-9 | On an entry's .qf-nav__row, between the item and its .qf-nav__action, when the user may add to that entry: a button labelled “New in <entry>” holding the circle-plus-filled icon. The row gains an add column. Shown while the row is hovered or holds focus, always without hover, never in organize mode or the collapsed rail. The row menu offers the same action as “New…”. |
.qf-nav[data-mode="organize"] |
D-5 | Organize mode. The mode control is a button.qf-nav__item[aria-pressed]; rows are buttons that open their menu; rows in a section take the grab cursor (the row is its own handle), entry rows do not. Every group is open, so a group row's button is a menu button like the other rows (aria-haspopup="menu", aria-controls the row menu, aria-expanded the menu's state), and the chevron shows the group open from the mode. |
.qf-nav__footer |
D-5 | Classic. The rail's own controls (the Organize mode control, then Maintain in organize mode) as .qf-nav__item buttons in a .qf-nav__footer placed after .qf-nav__items in the .qf-nav. It stays pinned to the bottom of the rail; the rows scroll above it and pass under its edge, so it never covers a row or a focused control. An empty footer takes no room. |
.qf-nav__skeleton, aria-busy="true", .qf-nav__twisty:disabled |
D-8 | Loading: skeleton rows (aria-hidden, each a .qf-skeleton[data-shape="text"]) in a busy .qf-nav__subitems or .qf-nav__items; an entry's twisty is disabled while its searches load. |
.qf-nav__empty |
D-8 | A <p> in a section with nothing in it, e.g. "No saved searches". |