# Icon Contract

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

Source: [https://askrjs.com/docs/components/icon-contract](https://askrjs.com/docs/components/icon-contract)

Status: stable. Packages: @askrjs/ui, @askrjs/themes, @askrjs/lucide.

**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.

## Example

```tsx
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.

## 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.

## Documentation navigation

[Previous](https://askrjs.com/docs/components/aria-and-ref-utilities/index.md) | [Next](https://askrjs.com/docs/components/button-and-button-group/index.md)
