Doc Page

Best Practices

Practical guidance for building stable, accessible contract-first primitives.

Updated: 2026-06-04 · Owner: docs@nake-ui

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:

  • button for triggers and command items.
  • input for text entry and hidden form values.
  • form and submit for 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:

  • open and closed through data-state, aria-expanded, and hidden.
  • selected through data-selected and aria-selected.
  • highlighted through data-highlighted, roving tabIndex, or aria-activedescendant.
  • disabled through native disabled when valid plus aria-disabled for non-native roles.
  • invalid through aria-invalid and data-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, or data-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-controls points to a missing panel ID.
  • Fix: render the panel with the referenced id, or update aria-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:contracts

In 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