ARIA and Ref Utilities
Use aria and ref utilities with the current published Askr packages.
@askrjs/ui0.0.13@askrjs/themes0.0.13
How to use aria and ref utilities
ARIA and Ref Utilities should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.
- Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
- Keep aria and ref utilities labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
- Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Button, Input } from '@askrjs/themes/components';
import { SearchIcon } from '@askrjs/lucide';
<label for="project-search">Search projects</label>
<Input id="project-search" name="query" />
<Button aria-label="Run search"><SearchIcon aria-hidden="true" /></Button>Purpose
Two patterns repeat across almost every component in @askrjs/ui: the `asChild` prop for rendering onto a caller-supplied element instead of the component's default tag, and a shared `Ref<T>` type for forwarding refs through that polymorphism. This page covers both, plus VisuallyHidden, the one component whose entire purpose is an accessibility utility rather than an interactive widget.
Install and import
`asChild` and `ref` aren't separately imported — they're just props defined on every component's prop union, e.g. `ButtonProps = ButtonNativeProps | ButtonAsChildProps`. `VisuallyHidden` is a normal component import: `import { VisuallyHidden } from '@askrjs/ui'`.
Live examples
`<Button asChild><a href="/docs">Docs</a></Button>` renders an anchor with button press semantics instead of a nested `<button><a>...</a></button>`, which keeps the DOM valid and avoids nested interactive elements. `<VisuallyHidden>Loading results</VisuallyHidden>` renders text that's readable by screen readers but not visible on screen, commonly paired with a spinner or icon-only control that needs an accessible name.
Anatomy
Every polymorphic component ships two prop types — a native variant (e.g. `ButtonNativeProps`, with `asChild?: false` and a native `type` attribute) and an as-child variant (`ButtonAsChildProps`, with `asChild: true`, a required single-element `children`, and `type?: never`) — combined into one discriminated union. `VisuallyHidden` follows the exact same split: `VisuallyHiddenSpanProps` for the default `<span>` and `VisuallyHiddenAsChildProps` for rendering onto something else.
State model
Neither `asChild` nor ref forwarding carries state — they're purely about which DOM element a component's behavior attaches to. State-bearing props like `checked` or `value` work identically regardless of which branch of the union you're in.
Keyboard and accessibility
When `asChild` renders onto a non-natively-interactive element (a `<div>` or `<a>` standing in for a button), the component compensates by adding `aria-disabled` and `tabindex="-1"` in place of the native `disabled` attribute, and by ensuring the element is keyboard-focusable — this is exactly the branch `BUTTON_A11Y_CONTRACT.DISABLED_ATTRIBUTES.asChild` documents. `VisuallyHidden` content stays in the accessibility tree and the tab order (when focusable) even though it's clipped from the visual viewport.
Styling and tokens
`asChild` doesn't add any wrapper markup for a theme to accidentally pick up — the component's `data-*` attributes and classes land directly on the element you provided as `children`. `VisuallyHidden` uses the standard clip-to-1px-and-hide-overflow technique rather than `display: none`, which is what keeps it perceivable to assistive tech while invisible on screen.
API
The shared `Ref<T>` type comes from `@askrjs/askr/foundations/utilities` and is used as `ref?: Ref<HTMLButtonElement>` (native branch) or `ref?: Ref<unknown>` (asChild branch, since the underlying element type isn't statically known). `JSXElement`, from `@askrjs/askr/foundations/structures`, types the single required child every `asChild` variant expects.
Edge cases
`asChild` requires exactly one child element — passing text, a fragment, or multiple children where a single element is expected will not compose correctly, since the component clones props onto that one element rather than wrapping a list. Because the asChild branch types `ref` as `Ref<unknown>`, you lose compile-time element-type checking on the ref target; that's a real trade-off of the polymorphism, not an oversight to work around.
Related pages
See Interaction Policies for how each component's `*_A11Y_CONTRACT` documents the asChild-vs-native attribute differences shown here. See Icon Contract for another small, focused accessibility convention that layers on top of components like Button.