# Headless versus Themed

> Headless versus Themed: anatomy, keyboard behavior, state, and theming in Askr.

Source: [https://askrjs.com/docs/components/headless-versus-themed](https://askrjs.com/docs/components/headless-versus-themed)

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, ThemeScope, ThemeToggle } from '@askrjs/themes/components';

<ThemeScope defaultTheme="system" storageKey="app-theme">
  <Button>Save project</Button>
  <ThemeToggle aria-label="Change color theme" />
</ThemeScope>
```

## Purpose

Separating behavior from appearance means you can swap the entire visual layer — or write none at all — without re-implementing focus traps, keyboard navigation, or ARIA attributes. @askrjs/ui gives you that behavior as unstyled primitives; @askrjs/themes gives you a finished, opinionated visual layer built on top of it. Most apps should start with @askrjs/themes and drop to @askrjs/ui directly only when the shipped visuals don't fit.

## Install and import

Install both packages together: `npm install @askrjs/ui @askrjs/askr` followed by `npm install @askrjs/themes`. Import the full surface from the package root (`import { Button, Dialog } from '@askrjs/ui'`) when convenience matters more than bundle size, or use per-component subpaths (`import { Button } from '@askrjs/ui/button'`, `import { Button } from '@askrjs/themes/button'`) when you want smaller, more explicit imports.

## Live examples

`@askrjs/ui/button` gives you an unstyled Button with real press handling and keyboard support but no CSS at all. `@askrjs/themes/button` gives you a Button that looks finished out of the box — because under the hood it wraps the exact same headless Button and layers styling and data-attribute hooks on top. Swap one import for the other and the behavior doesn't change, only the appearance does.

## Anatomy

For components that exist in both packages, the themed version is a thin wrapper: it renders the headless component and attaches classes and data attributes for styling. For components that exist only in @askrjs/themes — Tabs, Sidebar, Card, Badge, Combobox, and others — there's no wrapping happening at all, because there's nothing headless to wrap. Those components implement their own behavior directly inside the themed package.

## State model

When a headless counterpart exists, state — open or closed, checked, pressed, focused — is owned entirely by the @askrjs/ui component and exposed through `data-state` and related attributes. The themed wrapper never re-derives or duplicates that state; it just reads the same attributes to decide how to render. That's why styling a themed component's states means targeting `data-state` values rather than guessing at internal flags.

## Keyboard and accessibility

Keyboard handling and ARIA wiring are implemented exactly once, in @askrjs/ui, against a documented contract — Dialog exposes `DIALOG_A11Y_CONTRACT` with its role, `aria-modal`, `aria-labelledby`, and trigger attributes; Select exposes `SELECT_A11Y_CONTRACT` with its roles and `aria-selected` wiring. Themed components inherit this for free when they wrap a headless primitive, which is a big part of why the split exists in the first place. Components without a headless counterpart have to implement their own accessibility from scratch, so scrutinize those more carefully.

## Styling and tokens

@askrjs/themes styles components entirely through public `data-slot` and `data-state` hooks and `--ak-*` CSS custom properties, never by reaching into internal DOM structure. That's true whether or not a component has a headless counterpart — the styling contract is the same either way, so switching a component from headless-backed to themes-only doesn't change how you'd theme it.

## API

Headless primitives in @askrjs/ui commonly support an `asChild` prop, which renders their behavior and ARIA attributes onto a child element you provide instead of injecting an extra wrapper node — FocusScope, DismissableLayer, and VisuallyHidden all support it, alongside the interactive controls. Themed components generally don't need `asChild` themselves since they already compose the primitive underneath, but understanding the pattern matters once you start pairing @askrjs/ui directly with your own markup.

## Edge cases

Don't go looking for an @askrjs/ui subpath for every themed component — Tabs, Sidebar, Card, Badge, Combobox, Calendar, Drawer, Sheet, Native Select, and Input OTP have none, and importing `@askrjs/ui/tabs` will simply fail because it doesn't exist. If you need headless behavior for one of these, you'll have to build it yourself on top of the lower-level primitives like FocusScope and DismissableLayer rather than expecting a ready-made export.

## Related pages

See Composition and Accessibility for how the shared behavior primitives compose, and Structures for how the themed layout system (Block, Container, and friends) fits around all of this.

## Documentation navigation

[Previous](https://askrjs.com/docs/components/index.md) | [Next](https://askrjs.com/docs/components/composition-and-accessibility/index.md)
