Askr
UI & Components

Theme Tokens, Modes, and Direction

Use theme tokens, modes, and direction with the current published Askr packages.

  • @askrjs/ui0.0.13
  • @askrjs/themes0.0.13

How to use theme tokens, modes, and direction

Theme Tokens, Modes, and Direction should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.

  1. Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
  2. Keep theme tokens, modes, and direction labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
  3. 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

Tokens are the single source of truth for color, spacing, radius, and every other visual value in the default theme, so changing an app's look is mostly a matter of overriding CSS custom properties rather than component code. Modes layer on top of tokens to switch between light, dark, system, and the shipped cat-themed presets, and a small `Direction` component handles setting the `dir` attribute on a scoped subtree.

Install and import

Pull in the default theme with `@import "@askrjs/themes/default";` in your app stylesheet, and add `@import "@askrjs/themes/presets";` if you want the additional cat theme family. For the JavaScript side, `import { ThemeScope, ThemeToggle, ThemePicker, theme } from '@askrjs/themes/theme'`, and `import { Direction } from '@askrjs/themes/components'`.

Live examples

Wrap your app in `<ThemeScope><ThemeToggle>{({ nextTheme }) => nextTheme}</ThemeToggle></ThemeScope>` to get a working light/dark/system toggle with zero extra state management. After importing the presets layer, set `data-theme="tabby"` (or `ginger`, `tuxedo`, `calico`, `torty`) on an ancestor element to switch to one of the cat theme presets.

Anatomy

`ThemeScope` establishes a reactive scope that tracks the current theme and persists it under a `storageKey`; the `theme()` function reads and writes that same scope from anywhere inside it. Token overrides are plain CSS — `:root { --ak-color-primary: purple; --ak-radius-md: 12px; }` — and apply globally or per-subtree depending on where you write the selector. `Direction` is a separate, much simpler piece: it just wraps its children, sets the `dir` attribute, and emits a `data-slot="direction"` hook.

State model

`theme()` returns a `ThemeScopeValue` with `theme()` to read the current value, `setTheme()` to change it, the available `themes` list, and the `storageKey` it persists under. That's the entire state surface — there's no separate dark-mode flag to manage independently, since dark is just one more value in the same `ThemeName` union alongside `light`, `system`, and the cat preset names.

Keyboard and accessibility

`ThemeToggle` is built on the real Button component (it accepts `ButtonNativeProps` minus `children` and `onPress`), so it's a genuine, keyboard-operable button by default rather than a custom clickable div. It intentionally ships with no built-in icons — you supply `lightIcon`, `darkIcon`, and `systemIcon` yourself, and the rendered content is wrapped in `data-slot="theme-toggle-content"` so mixed icon-and-text compositions stay stylable.

Styling and tokens

The token contract spans color, typography, spacing, radius, borders, shadows, focus, motion, icons, layout, and z-index, all under the `--ak-*` prefix — for example `--ak-color-primary`, `--ak-space-*`, `--ak-radius-*`, and `--ak-focus-*`. Icon sizing specifically resolves through `--ak-icon-size-sm|md|lg|xl` and `--ak-icon-stroke-width-sm|md|lg|xl`, which lets you retune icon scale globally without touching individual components.

API

`ThemeName` is `"light" | "dark" | "system" | CatThemeName | (string & {})`, where `CAT_THEME_NAMES` is the readonly tuple `["tabby", "ginger", "tuxedo", "calico", "torty"]`. `ThemeScopeProps` takes `children`, `defaultTheme`, `themes`, and `storageKey`; `ThemeToggleProps` adds `lightIcon`, `darkIcon`, `systemIcon`, and a `themes` list on top of the Button props it extends; `ThemePickerProps` wraps a native `<select>` minus `value`, `defaultValue`, and `onChange`. `DEFAULT_THEME_OPTIONS` and `CAT_THEME_OPTIONS` are the shipped option lists for each.

Edge cases

`Direction` is a thin wrapper that sets `dir` and a `data-slot` hook — it is not a full bidi engine, so getting mirrored layouts right in RTL still depends on how logical properties are used throughout your own CSS. Also watch `storageKey` collisions: if two apps sharing a domain both use the default key, their theme preferences will clobber each other in storage, so set an explicit `storageKey` per app.

See Customization for the broader selector and override contract these tokens participate in, and Structures for how layout tokens specifically feed into the Block system.