Skip to content

Behavioral primitives

P-11 · P-12 · P-14 · P-15–P-20 · Primitives

All elements · Behavioral primitives on the Primitives page

Awaiting approval · Accessibility tests

Copies the bun run a11y:approve command for Behavioral primitives.

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

Do
  • Keep one tab stop per composite widget and let arrow keys move within it.
  • Mirror every data-keytip as aria-keyshortcuts on the control.
  • Keep announcements short and specific ("Bold on", "3 of 27").
  • Shed ribbon labels and move controls into the trailing menu by data-priority before ever scrolling horizontally.
Don’t
  • 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

Behavioral primitives 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, listbox or grid; items get tabindex="0" (current) or -1.
  • useCombobox: aria-expanded on the trigger, role="listbox", .qf-listbox__option[data-state="active"] for virtual focus, aria-selected, and aria-multiselectable for multi-select.
  • KeyTip badges (.qf-keytip) are aria-hidden; the shortcut is exposed as aria-keyshortcuts.
  • AccessibleMirror uses role="document" with aria-roledescription="page", role="grid" with aria-rowcount and aria-colcount, and role="group" with aria-roledescription="slide".
  • TextInputProxy carries aria-label ("Editing cell G13") and aria-describedby to the mirror node; LiveAnnouncer uses .qf-office-app__live with role="status" and role="alert".
  • DragSource states are data-state="dragging", "over" and "overlay"; the handles are .qf-list__handle, .qf-resizable__handle and .qf-table__resize.

Focus

  • RovingFocus skips hidden and inert items and updates the tab stop on focusin, 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 point aria-activedescendant at 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 scrollToIndex it 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-label and 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

Contract not yet reviewed

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.

Behavioral primitives package
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.

All release notes