Doc Page
Primitive Concepts
Learn the shared state and event principles behind all primitives.
Mental Model
A primitive is a behavior contract with a public DOM shape. It is not a styled component. The controller decides state transitions, the adapter maps them to framework syntax, and the DOM reflects the result for users, assistive technology, tests, and agents.
Every primitive has four layers:
- Anatomy: named slots and their relationships.
- State: controlled/uncontrolled inputs and reflected output attributes.
- Interaction: native events, keyboard model, focus policy, and dismissal rules.
- Validation: schema-backed rules that report broken structure or semantics.
Sources: shared/en/primitive-concepts
Anatomy
Slots are public. If a spec names trigger, content, option, panel, or hidden-input, the rendered DOM should expose that slot through data-slot.
Slot rules:
- Required slots must exist.
- Repeatable slots need stable identity.
- Optional slots still follow public naming when rendered.
- Parent/child relationships matter for validation.
- Portal boundaries do not erase ownership relationships.
An agent should be able to inspect markup and answer "which primitive is this, which slot is this node, and what state is it in?"
Sources: shared/en/primitive-concepts
State
Primitives support controlled and uncontrolled state when interaction complexity requires it.
Controlled examples:
open+onOpenChangefor Dialog, Popover, Menu, Select, Combobox, Tooltip.value+onValueChangefor Tabs, Listbox, Select.inputValue+onInputValueChangefor Combobox.
Uncontrolled examples:
defaultOpendefaultValuedefaultInputValue
Do not pass both controlled and uncontrolled values for the same state dimension after initial implementation. The validator treats controlled/uncontrolled conflicts as contract bugs because they create ambiguous truth.
Sources: shared/en/primitive-concepts
Reflected Attributes
Important state must be visible in DOM:
| State concept | Typical reflection |
|---|---|
| open/closed | data-state, aria-expanded, hidden |
| selected | data-selected, aria-selected |
| highlighted | data-highlighted, aria-activedescendant or roving tabIndex |
| disabled | data-disabled, aria-disabled, native disabled when valid |
| invalid | data-invalid, aria-invalid |
| orientation | data-orientation, aria-orientation where applicable |
| floating layout | data-placement, data-side, data-align |
If state is visible only through CSS classes, the primitive is not agent-friendly enough.
Sources: shared/en/primitive-concepts
Event Model
The event model follows the web platform:
- Pointer and mouse:
pointerdown,mousedown,click,mouseenter,mouseleave. - Focus:
focus,blur,focusin,focusout. - Keyboard:
keydownwith named key handling. - Forms:
input,change,submit.
Adapter callbacks should describe state changes, but they should not replace platform events as the underlying model.
Sources: shared/en/primitive-concepts
Keyboard And Focus
Complex primitives require explicit keyboard behavior:
- Tabs: roving focus,
Arrow*,Home,End, optional manual activation withEnter/Space. - Dialog: focus trap in modal mode, LIFO close behavior, focus restoration.
- Menu: roving focus, typeahead, submenu open/close,
Escape. - Listbox/Select: option navigation and selection commit.
- Combobox: focus stays on input while
aria-activedescendantpoints at highlighted options. - Tooltip: trigger keeps focus, content is not tabbed into by default.
Do not ship a primitive that "looks clickable" but cannot be operated from the keyboard.
Sources: shared/en/primitive-concepts
ARIA
Use native elements first, then ARIA only where native semantics are insufficient.
Common relationships:
aria-controls: trigger/input points to content/listbox/panel.aria-labelledby: content/panel points back to label/title/trigger.aria-describedby: trigger or content points to helper text.aria-selected: option or tab selected state.aria-activedescendant: active option while DOM focus remains on input/listbox.aria-modal: modal dialog content.
Every ARIA ID reference should resolve to an element in the rendered DOM after hydration.
Sources: shared/en/primitive-concepts
SSR And Hydration
Server output must match the client contract:
- IDs are deterministic.
- Initial state is the same on server and client.
- Client-only measurement waits until mount.
- Hidden content stays hidden until activated.
- Portal targets do not break ARIA linkage.
Hydration mismatches are not just React warnings. They can break validator repair, focus restoration, and screen reader relationships.
Sources: shared/en/primitive-concepts
Validator Language
Validator messages should include:
- Rule ID.
- Trigger condition.
- Fix suggestion.
- Whether the issue can be auto-fixed.
Example issue shape:
{
"ruleId": "aria.broken-controls-linkage",
"trigger": "Trigger aria-controls points to a missing content id.",
"suggestion": "Render the content slot with the referenced id or update aria-controls.",
"canAutoFix": false
}Sources: shared/en/primitive-concepts
Checklist
- Required slots are present.
- State has one source of truth.
- ARIA relationships resolve.
- Keyboard model is documented and tested.
- Focus behavior is deterministic.
- Public
data-*attributes match the schema.
Sources: shared/en/primitive-concepts