Doc Page
Best Practices
Practical guidance for building stable, accessible contract-first primitives.
Contract First
Design the DOM contract before polishing visuals. A stable primitive answers these questions:
- Which slots are public?
- Which states exist?
- Which attributes reflect those states?
- Which keys move focus or commit values?
- Which ARIA relationships must resolve?
- Which validator rules catch broken markup?
If the answer only exists in component internals, tests and agents cannot reliably inspect or repair the UI.
A useful workflow is to write the smallest valid DOM example before designing the framework wrapper. Keep required slots, roles, ARIA, and reflected state visible first; then let each adapter provide its ergonomic surface without changing the contract.
During review, ask:
- Does the primitive have a clear root or anchor?
- Can the validator report a missing required slot with a concrete fix?
- Do controlled and uncontrolled usage share the same state transitions?
- Is the SSR output accessible before client activation?
- Can a user, test, or agent read the current state without reading source code?
Sources: shared/en/best-practices
Prefer Native Semantics
Use native elements whenever they fit:
buttonfor triggers and command items.inputfor text entry and hidden form values.formandsubmitfor form flows.- Native focus and keyboard events before custom abstractions.
ARIA should fill gaps, not overwrite useful native semantics.
Common mistakes include using div role="button" without Enter and Space, replacing native select behavior without a label or form value, and setting aria-disabled while still allowing a command handler to run. If the platform element already expresses the intent, start there.
Sources: shared/en/best-practices
Keep State Observable
Reflect state consistently:
openandclosedthroughdata-state,aria-expanded, andhidden.selectedthroughdata-selectedandaria-selected.highlightedthroughdata-highlighted, rovingtabIndex, oraria-activedescendant.disabledthrough nativedisabledwhen valid plusaria-disabledfor non-native roles.invalidthrougharia-invalidanddata-invalid.
Classes may mirror state for styling, but they should not be the only state channel.
Reflected state should match what users can perceive. If a Popover appears open, the trigger should expose expanded/open state and the content should expose matching data-state. If an option highlight exists only as an index in a closure, keyboard tests, screen reader feedback, and repair tools all become fragile.
Define the truth source before composing layers:
- Controlled: the external prop is truth; the callback requests updates.
- Uncontrolled: the controller owns state, but still reflects it to DOM.
- Derived layout: positioning can be computed on the client, but resolved placement should be exposed through
data-placement,data-side, ordata-align.
Sources: shared/en/best-practices
Test Behavior, Not Only Render
Minimum test layers:
- Unit tests for controller transitions.
- Contract snapshots for public DOM attributes.
- Adapter tests for rendered roles, callbacks, and controlled/uncontrolled parity.
- Interaction tests for keyboard, pointer, focus, outside dismiss, and nested layers.
- A11y checks for labels, roles, and relationships.
High-risk bugs often appear only when pointer, focus, and portal behavior cross.
Layer tests by failure type:
| Failure type | Preferred test location |
|---|---|
| State transition bug | primitive/controller unit test |
| Slot or ARIA output bug | adapter contract test |
| Cross-adapter drift | parity test |
| Docs/example route breakage | site unit/e2e test |
| Schema or validator message bug | schemas/devtools tests |
Do not test only the happy path. Dialog close behavior, same-value Select activation, Menu submenus, Combobox IME, and portalled Select inside Popover are the paths that tend to protect real users.
Sources: shared/en/best-practices
Document Failure Modes
Good docs include what not to do:
- Missing label.
- Broken
aria-controls. - Duplicate roving tab stop.
- Controlled/uncontrolled conflict.
- Hidden content still reachable.
- Portalled child incorrectly treated as outside.
Failure cases make docs useful for agents and validators, not just humans.
Write failure modes as symptom, cause, and fix:
- Symptom: Tabs panel is not announced.
- Cause: the active tab
aria-controlspoints to a missing panel ID. - Fix: render the panel with the referenced
id, or updatearia-controls.
That format is readable for humans and easy for agents to translate from validator output into code edits.
Sources: shared/en/best-practices
Adapter Parity
Maintain parity by sharing behavior in core and keeping adapters thin. Adapter differences should be ergonomic:
- React and Solid use JSX components.
- Vue uses components/composables.
- Svelte uses actions.
- Vanilla binds explicit elements.
Differences should not change public state, ARIA, keyboard, or focus behavior.
Before adding adapter-specific API, prove it does not change the contract:
- Public slot names remain the same.
- The same event causes the same state transition.
- Disabled, invalid, selected, and highlighted state reflect in the same places.
- Cleanup leaves no listener, portal node, or stale registration behind.
- Initial IDs and initial state are stable across SSR and hydration.
Sources: shared/en/best-practices
Copy-paste Checks
When copying from examples, copy the complete structure first, then remove optional styling. Pay special attention to:
- Labels, descriptions, hidden inputs, and
aria-*linkage. - Vanilla binding return values and cleanup.
- Forwarded refs, IDs, adapter handlers, and collection registration in framework wrappers.
- Public
data-*attributes when adding local wrappers. - DOM inspection before CSS customization.
After copying, run at least:
pnpm content:check
pnpm test:contractsIn an application, also do a keyboard smoke pass: Tab reaches the expected target, arrows move, Enter/Space commits, and Escape closes the correct layer.
Sources: shared/en/best-practices
Checklist
- The primitive can be inspected from DOM alone.
- Native elements are used where appropriate.
- State is reflected with ARIA plus public
data-*. - Keyboard and focus behavior is written down.
- Examples cover controlled and uncontrolled mode where relevant.
- Validator messages include rule ID, trigger, suggestion, and auto-fix status.
Sources: shared/en/best-practices