G · 10 guides

Guides / Primitives

Primitives

@aihu/primitives is a set of headless behavior primitives — WAI-ARIA APG patterns implemented as vanilla custom elements. Each emits DOM structure, ARIA wiring and data-state attributes, and owns its state on @aihu/signals.

Every primitive ships zero CSS. You style every part yourself, typically with the css-engine's utilities and cn().

Why headless

A headless primitive gives you the hard part — focus management, keyboard interaction, ARIA roles and relationships, open/close state — without imposing a look. Each piece reflects data-state="open" | "closed" and friends, so your CSS selectors drive appearance while the primitive guarantees the accessibility contract.

This mirrors the Radix/Ark root-and-pieces model: a root owns the state and provides it through a DOM-walk context; pieces inject it by walking up the real DOM, across shadow boundaries, nearest-provider-wins.

The behaviors

Overlays

Dialog — the APG Modal Dialog pattern. Pieces: <aihu-dialog-root>, -trigger, -content, -backdrop, -close, -title, -description. Focus trap with return-focus, Escape to close, outside-click to close when modal, and the full role="dialog" / aria-modal / aria-labelledby / aria-describedby set plus the trigger's aria-haspopup / aria-expanded / aria-controls. defineDialog().

Popover — a non-modal disclosure. The trigger follows APG Disclosure wiring (aria-expanded + aria-controls); the panel takes the dialog role without aria-modal, and focus is not trapped. That difference is the whole point: use Popover when the page behind stays live, Dialog when it must not. definePopover(prefix = 'aihu') — note it takes a tag prefix, not a tag.

Tooltip — the APG Tooltip pattern. The trigger is aria-describedby the content (not labelled-by), the content is role="tooltip" and not focusable, Escape dismisses. Configurable open-delay / close-delay (700 / 300 ms). Placement reuses the css-engine's position() shim, so there is no floating-ui dependency. defineTooltip().

Form controls

Checkbox — APG Checkbox, including the tri-state indeterminate case. <aihu-checkbox-root> plus an <aihu-checkbox-indicator> styling hook. defineCheckbox().

Switch — APG Switch. Deliberately a sibling of Checkbox rather than shared code: the ARIA contracts genuinely diverge (binary vs tri-state, and Enter behaves differently). <aihu-switch-root> + <aihu-switch-thumb>. defineSwitch().

Radio group — APG Radio Group, built by extending <aihu-roving-focus>: the root is the roving-tabindex container. Pieces: root, -item (role="radio"), -indicator. defineRadioGroup().

Slider — APG Slider, single-thumb. defineSlider().

Input / Textarea — text controls over a shared base, wired into the form-control context below. defineInput(tag = 'aihu-input'), defineTextarea(tag = 'aihu-textarea').

Label — label association that survives shadow boundaries, where a native <label for> would not. defineLabel(tag = 'aihu-label').

ButtonAihuButton, the APG Button pattern as a base class. On a non-native host it sets role="button" + tabindex="0" and handles Enter/Space to fire a synthetic click; a native <button> defers to native semantics. Reflects aria-pressed, aria-disabled, data-state, and inherits disabled from an ancestor form-control. It is a base class, not a pre-registered tag — extend it, or register a concrete element with defineButton(tag).

Separatorrole="separator" with orientation. defineSeparator(tag = 'aihu-separator').

Substrates

The lower-level pieces the behaviors compose on. Use them directly when building your own:

Primitive Subpath Role
DOM context @aihu/primitives/context Live ancestor-traversal context. The root-to-piece coordination mechanism. Self-contained — does not import @aihu/context.
Focus trap @aihu/primitives/focus-trap Tab-cycle containment with return-focus. Its own subpath, so you can trap focus without a dialog.
Presence gate @aihu/primitives/presence-gate Holds children mounted through an exit transition.
Roving focus @aihu/primitives/roving-focus Roving-tabindex management for composite widgets, configurable orientation.
Collection @aihu/primitives/collection Ordered registration of descendant items.
Config provider @aihu/primitives/config-provider Propagates colorScheme / density / direction to descendants.
Form control @aihu/primitives/form-control Shared label / description / validity wiring, plus the disabled context descendants inherit.

Styling them

Primitives are unstyled, so you bring the CSS. Style each data-state with utilities, and merge caller overrides with cn():

governedtsts198 B
import { defineDialog } from '@aihu/primitives/dialog'
import { cn } from '@aihu/css-engine/runtime/cn'

defineDialog()

const contentClass = cn('rounded-lg p-6 bg-surface shadow-lg', userClassName)
governedhtmlhtml457 B
<aihu-dialog-root>
  <aihu-dialog-trigger>Open</aihu-dialog-trigger>
  <aihu-dialog-backdrop class="fixed inset-0 bg-black/40 data-[state=closed]:opacity-0"></aihu-dialog-backdrop>
  <aihu-dialog-content class="rounded-lg p-6 bg-surface shadow-lg">
    <aihu-dialog-title>Title</aihu-dialog-title>
    <aihu-dialog-description>Body copy.</aihu-dialog-description>
    <aihu-dialog-close>Close</aihu-dialog-close>
  </aihu-dialog-content>
</aihu-dialog-root>

The primitive guarantees the focus trap, Escape, ARIA and data-state; your utilities supply the look.

When not to hand-roll

@aihu/ui is a registry of styled components built on these primitives — you copy a component into your project and own the source, rather than depending on a black box. If you want a styled dialog, switch or slider rather than a headless one, start there and reach for primitives when you need a behavior the registry does not cover.

See also