Askr
UI & Components

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.

  1. Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
  2. Keep icon contract 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, 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.

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.