Askr documentation
UI & Components

Icon Contract

Icon Contract: anatomy, keyboard behavior, state, and theming in Askr.

Example

import { Button, Input } from '@askrjs/themes/components';
import { SearchIcon } from '@askrjs/lucide';

<>
  <label htmlFor="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.