Headless versus Themed
Use headless versus themed with the current published Askr packages.
@askrjs/ui0.0.13@askrjs/themes0.0.13
How to use headless versus themed
Headless versus Themed 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 headless versus themed 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, 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.