# ARIA and Ref Utilities

> ARIA and Ref Utilities: anatomy, keyboard behavior, state, and theming in Askr.

Source: [https://askrjs.com/docs/components/aria-and-ref-utilities](https://askrjs.com/docs/components/aria-and-ref-utilities)

Status: stable. Packages: @askrjs/ui, @askrjs/themes.

**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.

## Example

```tsx
import { Button, Input } from '@askrjs/themes/components';
import { SearchIcon } from '@askrjs/lucide';

<>
  <label htmlFor="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<Element>` (asChild branch — narrower than the native branch's specific element type, but still the base DOM `Element` type, not `unknown`). `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 the base `Ref<Element>` rather than the native branch's specific `Ref<HTMLButtonElement>`, you lose the specific-element-type checking on the ref target (methods/properties unique to the concrete element type won't be visible without a cast); 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.

## Documentation navigation

[Previous](https://askrjs.com/docs/components/controlled-state/index.md) | [Next](https://askrjs.com/docs/components/icon-contract/index.md)
