Askr
UI & Components

Interaction Policies

Use interaction policies with the current published Askr packages.

  • @askrjs/ui0.0.13
  • @askrjs/themes0.0.13

How to use interaction policies

Interaction Policies should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.

  1. Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
  2. Keep interaction policies labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
  3. Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Button, Dialog, DialogContent, DialogTitle, DialogTrigger } from '@askrjs/themes/components';

<Dialog>
  <DialogTrigger asChild><Button>Open settings</Button></DialogTrigger>
  <DialogContent>
    <DialogTitle>Project settings</DialogTitle>
    <ProjectSettings />
  </DialogContent>
</Dialog>

Purpose

Every interactive @askrjs/ui component ships a companion `*_A11Y_CONTRACT` object — `BUTTON_A11Y_CONTRACT`, `ACCORDION_A11Y_CONTRACT`, `SWITCH_A11Y_CONTRACT`, and so on — that documents its ARIA role, keyboard bindings, and data attributes as a typed, importable value rather than as prose you have to trust. This page explains what these contracts are for and how to read them, since they're the closest thing @askrjs/ui has to a formal interaction spec per component.

Install and import

Contracts are exported alongside their component from the same entrypoint, e.g. `import { Button, BUTTON_A11Y_CONTRACT } from '@askrjs/ui'` or from the per-component subpath `import { BUTTON_A11Y_CONTRACT } from '@askrjs/ui/button'`. You don't need them at runtime to use a component correctly — they're most useful for testing and for verifying your own asChild wrapper preserves the expected behavior.

Live examples

`BUTTON_A11Y_CONTRACT.KEYBOARD_ACTIVATION` is `['Enter', 'Space']` and `BUTTON_A11Y_CONTRACT.DISABLED_ATTRIBUTES` distinguishes the native `disabled` attribute from the `aria-disabled` used when `asChild` renders something other than a real `<button>`. `DISMISSABLE_LAYER_A11Y_CONTRACT.DISMISS_EVENTS` lists `['escape-key', 'outside-pointer']`, which is exactly the set of interactions DismissableLayer listens for by default.

Anatomy

Each contract is a `declare const ..._A11Y_CONTRACT` frozen object with a matching type alias (`ButtonA11yContract`, `SwitchA11yContract`, etc.) derived via `typeof`, so you get autocomplete and type-checking against the literal values rather than a loosely typed record. Fields are grouped by concern — role, keyboard activation, disabled-state attributes, focus rules, and the `DATA_ATTRIBUTES` map of stable `data-*` hooks — and not every component populates every group, since not every component needs one.

State model

The contracts themselves are static — they don't change at runtime and carry no component state. What varies at runtime is which branch of a contract applies: Button's `DISABLED_ATTRIBUTES.native` kicks in for a real `<button>`, while `DISABLED_ATTRIBUTES.asChild` (`aria-disabled` plus `tabindex="-1"`) applies when `asChild` is set and the rendered element isn't natively disableable.

Keyboard and accessibility

This is the primary content of most contracts. `KEYBOARD_ACTIVATION` lists which keys trigger a press-like interaction (`Enter`/`Space` for Button and Switch), and `DISMISS_EVENTS` on DismissableLayer's contract lists which interactions close an overlay. Reading a component's contract before wiring custom keyboard handling around it tells you what's already handled internally versus what you still need to implement yourself.

Styling and tokens

`DATA_ATTRIBUTES` inside each contract is the styling-relevant part: it names the `data-slot`, `data-state`, and `data-disabled` (or similarly-purposed) attributes a component sets on its DOM output. `@askrjs/themes` selectors are written against exactly these attribute names, so if you build a custom theme, the contract tells you which hooks are guaranteed stable rather than incidental implementation detail.

API

There's no single generic contract type — each component defines its own, e.g. `ButtonA11yContract`, `AccordionA11yContract`, `SwitchA11yContract`, `DismissableLayerA11yContract`, `FocusScopeA11yContract`. Import the constant when you want the actual values (for tests or docs generation) and the type when you only need to constrain a function parameter to 'something shaped like a contract'.

Edge cases

Contracts describe intended behavior, not a runtime guarantee enforced by the library — nothing stops a consumer from overriding `role` or stripping a `data-*` attribute via passthrough props, so treat them as documentation-you-can-import rather than a validator. Some simpler components (Input, Textarea, Label) don't export a contract at all because they rely on native HTML semantics that don't need restating.

See ARIA and Ref Utilities for how these contracts relate to the `asChild` pattern and ref forwarding. See Focus and Dismissal for the two components — FocusScope and DismissableLayer — whose entire job is implementing one interaction policy each.