Icon Contract
Use icon contract with the current published Askr packages.
@askrjs/ui0.0.13@askrjs/themes0.0.13@askrjs/lucide0.0.4
How to use icon contract
Icon Contract 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 icon contract 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, Input } from '@askrjs/themes/components';
import { SearchIcon } from '@askrjs/lucide';
<label for="project-search">Search projects</label>
<Input id="project-search" name="query" />
<Button aria-label="Run search"><SearchIcon aria-hidden="true" /></Button>Purpose
@askrjs/ui has no Icon component and no dedicated icon prop type — every `icon` field you'll see on a themed component (Alert, EmptyState) is typed as plain `unknown`, meaning it accepts whatever renderable you hand it. What ties icons together across the design system instead is a shared markup and CSS contract: any SVG carrying `data-slot="icon"` gets sized and styled consistently by `@askrjs/themes`, regardless of where it came from.
Install and import
There's nothing to import for the contract itself — it's a set of data attributes and CSS custom properties, not a component or type. In practice you'll satisfy it either by using icons from the separate `@askrjs/lucide` package, which emit the attributes automatically, or by adding `data-slot="icon"` to any custom SVG you use in an icon-accepting slot.
Live examples
A Button sized `"icon"`, `"icon-sm"`, or `"icon-lg"` with a single icon child (`data-slot="icon"`) gets that icon auto-sized via the theme's `--ak-icon-size-*` tokens without any extra CSS from you. `Alert`'s `icon` prop and `EmptyState`'s `icon` prop both accept the same kind of child — any renderable, typically an icon — and the theme's `[data-slot="icon"]` selectors apply consistent sizing wherever that child lands.
Anatomy
The default theme's CSS targets `[data-slot="icon"]` broadly (setting `inline-size`/`block-size` from `--ak-icon-size`) and then narrows further inside specific components — a button's icon child, a sidebar menu button's icon, a theme toggle's icon — each pulling from the same token scale rather than defining its own pixel values. `[data-decorative="true"]` is the companion attribute that marks an icon as non-semantic so it doesn't need to be exposed to assistive tech.
State model
Icons aren't stateful themselves; they're static children passed through props like `icon` or as button/toggle children. Any state that affects an icon's appearance — a toggle's checked state, a button's disabled state — is expressed on the *parent* element's `data-state`/`data-disabled` attributes, and theme selectors combine those with `[data-slot="icon"]` to restyle the icon (e.g. muted color when the parent is disabled).
Keyboard and accessibility
Icons used purely for decoration should carry `data-decorative="true"` (or, from `@askrjs/lucide`, simply omit a `title`, which sets that attribute automatically) so screen readers skip them. Icons that carry meaning on their own — an icon-only Button with no visible label — still need an accessible name from `aria-label` or a `VisuallyHidden` child; the icon contract doesn't provide one for you.
Styling and tokens
The default theme exposes `--ak-icon-size-sm/md/lg/xl` and matching `--ak-icon-stroke-width-*` custom properties, plus a generic `--ak-icon-size` that component-level selectors fall back to. Overriding icon size anywhere in the tree is a matter of setting one of these custom properties on an ancestor rather than targeting SVG dimensions directly.
API
The contract is attribute-based, not type-based: `data-slot="icon"` (identifies the element as an icon for styling), `data-icon="<Name>"` (identifies which icon, set by `@askrjs/lucide`), `data-decorative="true"` (hides from assistive tech when unset otherwise), `data-color="current"` (marks currentColor inheritance), and `data-size="sm|md|lg|xl"` for named sizes. None of these are exported as TypeScript types from `@askrjs/ui` or `@askrjs/themes` — they're a markup convention documented by `@askrjs/lucide` and consumed by the theme's CSS.
Edge cases
Because `icon` props are typed `unknown`, nothing stops you from passing an icon that doesn't carry `data-slot="icon"` — it'll render, but it won't pick up the theme's automatic sizing and you'll need to size it yourself. Mixing icons from `@askrjs/lucide` with hand-rolled SVGs in the same button size class can produce visibly inconsistent icon sizing if the hand-rolled ones don't opt into the same `data-slot`/CSS-variable contract.
Related pages
See ARIA and Ref Utilities for the `asChild` pattern icons are frequently composed through. See the Button and Toggle Family pages for concrete icon-only sizing examples.