Askr documentation
UI & Components

Headless versus Themed

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

Example

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

Headless UI components own keyboard handling, focus and ARIA relationships. Themed wrappers reuse that behavior. Verify the actual role, labeling, selection and focus outcomes for your composition, especially when using a styling-only component that has no headless counterpart.

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

Some themed components, including Tabs, Sidebar, Card, Badge, Combobox, Calendar, Native Select and Input OTP, have no matching headless subpath. Sheet uses the existing Dialog family for behavior and adds themed parts; use @askrjs/ui/dialog when composing that behavior directly.

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.