P-11 · P-12 · P-14 · P-15–P-20 · Primitives
All elements · Behavioral primitives on the Primitives page
Awaiting approval · Accessibility tests
Approve Behavioral primitives
Approving as
Behavioral primitives
P-11 · P-12 · P-14 · P-15–P-20Contract not yet reviewed
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". |
Guidance
When to use
- Window long homogeneous lists or tables with VirtualScroller (P-11) inside a ScrollArea.
- Move, sort or resize items with DragSource (P-12), always with a keyboard or menu path for the same result.
- Wire any combobox or listbox with useCombobox (P-14) before building a Select variant.
- Give a toolbar, tab list, listbox or grid one tab stop with RovingFocus (P-15), and add KeyTips (P-16) to the editor ribbon.
- Make QEditor canvases usable with TextInputProxy (P-17), AccessibleMirror (P-18), LiveAnnouncer (P-19) and AdaptiveCollapse (P-20).
When to consider something else
- When a list is short enough to render in full, use a plain List in a ScrollArea and skip virtualization. See List.
- When reordering is rare, offer move up and move down in a Menu instead of drag alone. See Menu.
- When the choice is a short fixed set with no typing, use a native-backed Select. See Select.
- When a message must be seen, not only heard, use Toast or Banner in addition to LiveAnnouncer. See Toast.
Usability
- Keep one tab stop per composite widget and let arrow keys move within it.
- Mirror every
data-keytipasaria-keyshortcutson the control. - Keep announcements short and specific ("Bold on", "3 of 27").
-
Shed ribbon labels and move controls into the trailing menu by
data-prioritybefore ever scrolling horizontally.
- Don't make drag the only way to reorder, move or resize (2.5.7).
- Don't announce every keystroke or repeat the same message inside the coalescing window.
- Don't let virtualization drop the focused row from the DOM.
Accessibility
Keyboard
| Keys | Action |
|---|---|
| Tab |
Moves into a [data-rove] container at its current item (tabindex="0"),
then out to the next widget.
|
| Arrow Left / Arrow Right |
Moves to the previous or next item in a horizontal [data-rove] container; a text
input with a caret keeps these keys. The demo wraps at the ends.
|
| Arrow Up / Arrow Down |
Moves within a container with aria-orientation="vertical"; a grid needs both axes
(2-D mode).
|
| Home / End | Moves to the first or last item in the container. |
| Ctrl+Arrow Left / Ctrl+Arrow Right | Jumps to the first control of the previous or next ribbon group. |
| Alt+Windows (Windows) or Ctrl+Option (macOS) | Shows KeyTips on File and the ribbon tabs. |
| Letter | With KeyTips shown, selects the matching tab and shows its panel's tips, or activates the matching control. |
| Escape | With KeyTips shown, steps back one layer, then hides them. |
Roles and names
-
RovingFocus containers are
role="toolbar",tablist,listboxorgrid; items gettabindex="0"(current) or-1. -
useCombobox:
aria-expandedon the trigger,role="listbox",.qf-listbox__option[data-state="active"]for virtual focus,aria-selected, andaria-multiselectablefor multi-select. -
KeyTip badges (
.qf-keytip) arearia-hidden; the shortcut is exposed asaria-keyshortcuts. -
AccessibleMirror uses
role="document"witharia-roledescription="page",role="grid"witharia-rowcountandaria-colcount, androle="group"witharia-roledescription="slide". -
TextInputProxy carries
aria-label("Editing cell G13") andaria-describedbyto the mirror node; LiveAnnouncer uses.qf-office-app__livewithrole="status"androle="alert". -
DragSource states are
data-state="dragging","over"and"overlay"; the handles are.qf-list__handle,.qf-resizable__handleand.qf-table__resize.
Focus
-
RovingFocus skips hidden and
inertitems and updates the tab stop onfocusin, so focus return lands on the last used item. -
useCombobox keeps DOM focus on the trigger and moves virtual focus by marking the option
data-state="active"; the binding must also pointaria-activedescendantat that option so screen readers follow it. - TextInputProxy holds focus while editing a canvas and must stay at the caret so IME windows appear in place.
-
VirtualScroller must keep the focused item rendered and
scrollToIndexit when focus moves by keyboard.
Other requirements
- Every drag outcome needs a single-pointer or keyboard alternative (2.5.7); the nav tree uses its row menu.
- LiveAnnouncer covers status messages (4.1.3): one polite and one assertive region per app, de-duplicated, with a one-second coalescing window.
-
AdaptiveCollapse keeps the name in
aria-labeland the tooltip when it sheds a label (2.5.3), and must not clip or overlap groups at 320 CSS px (1.4.10). - KeyTip letters are character shortcuts only while the layer is shown, which satisfies 2.1.4.
Test your implementation too. This guidance covers the element as specified here. How a screen composes it (labels, order, announcements) still needs its own keyboard and screen reader check.
Accessibility tests
No visual output for the gate to test. A person reviews the behavioral contract instead; not reviewed yet.
Approving. When the checklist passes on the checked run, record it with
bun run a11y:approve -- behavioral --by "Your name" and commit
a11y/approvals.json with the rebuilt pages.
All test status
Package
Consumers link one stylesheet, css/classic/index.css, which includes every element. The
files below are where this element is defined.
| Source | No stylesheet of its own |
|---|---|
| Specimens also use | Box · Chip · SideNavigation · Table · Text |
| Sprites | — |
| React | Not yet available: the React binding has not started. |
Latest updates
No recorded changes yet.