Icons
ai2 uses exactly two icon families, each with a fixed job - and every registry component lays out correctly with either one.
Two families, two jobs
- lucide-react - the consumer family. Registry components import lucide icons as PascalCase React components (Button's loading spinner is
Loader2, FileInput's default icon isPaperclip). This is what ships to your project via the shadcn CLI, so the API stays shadcn-compatible. - remixicon - the site chrome family. The ai2 site itself (navigation, marketing sections, docs chrome) renders font icons as
<i className="ri-*" />elements, with the remixicon CSS imported once in the root layout. Nothing from this family ships to consumers.
import { Plus, ArrowRight } from "lucide-react"
<Button tone="brand">
<Plus /> Add member
</Button>// Site chrome only - remixicon.css is imported once in the root layout.
<i className="ri-search-line" aria-hidden="true" />The dual-compatibility rule
Even though lucide is the shipped default, every registry component must lay out correctly with both a lucide <svg> and a remixicon <i> child. Concretely: any structural selector written against svg (has-[>svg], [&>svg], [&_svg]) carries the matching i pair. The pair is additive, so lucide consumers render identically. Alert, FileInput and InputGroup are the reference implementations:
// alert.tsx - the icon column opens for either family:
"has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>i]:grid-cols-[calc(var(--spacing)*4)_1fr]"
// file-input.tsx / input-group.tsx - sizing for either family:
"[&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none"Sizing conventions
- lucide - svg children default to
size-4(16px) inside components and are set toshrink-0, so icons line up without per-icon classes. Button only applies the default when the icon carries no size class of its own. - remixicon - a font glyph, so it is sized with text utilities:
text-baseplusleading-nonekeeps the glyph optically matched to a 16px svg without adding line height to the row.
Placement patterns
Button, icon before the label
import { Plus } from "lucide-react"
<Button variant="soft" tone="success">
<Plus /> Add member
</Button>Button, icon after the label
import { ArrowRight } from "lucide-react"
<Button tone="brand">
Get started <ArrowRight />
</Button>No wrapper or margin classes needed in either direction - the button's size axis already sets the gap between icon and label.
Icon-only buttons
Button ships five square sizes for icon-only use. Each pairs with a text size on the shared control height scale, so an icon button sits flush next to an md input. An icon-only button has no visible label, so aria-label is required, always.
| Size | Box | Pairs with |
|---|---|---|
| icon-xs | size-7 (28px) | xs |
| icon-sm | size-8 (32px) | sm |
| icon | size-9 (36px) | md |
| icon-lg | size-10 (40px) | lg |
| icon-xl | size-12 (48px) | xl |
import { Settings2 } from "lucide-react"
<Button size="icon-xs" variant="ghost" aria-label="Settings"><Settings2 /></Button>
<Button size="icon-sm" variant="ghost" aria-label="Settings"><Settings2 /></Button>
<Button size="icon" variant="outline" aria-label="Settings"><Settings2 /></Button>
<Button size="icon-lg" variant="soft" aria-label="Settings"><Settings2 /></Button>
<Button size="icon-xl" tone="brand" aria-label="Settings"><Settings2 /></Button>FileInput icon prop
import { Image } from "lucide-react"
// Default icon is a lucide Paperclip; swap it via the icon prop.
<FileInput accept="image/*" placeholder="Upload a cover image" icon={<Image />} />
// A remixicon <i> works in the same slot:
<FileInput placeholder="Attach a file" icon={<i className="ri-attachment-2" />} />InputGroup addons
Addons are the icon home in grouped inputs: the group sizes any svg or <i> inside, and clicking a passive addon focuses the control.
import { Search } from "lucide-react"
<InputGroup>
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupInput placeholder="Search components" />
<InputGroupAddon align="inline-end">
<InputGroupText>34 results</InputGroupText>
</InputGroupAddon>
</InputGroup>Do-not rules
- Never mix families within one surface. A toolbar, a card, a page section uses lucide or remixicon, not both. Mixed families differ subtly in stroke weight and optical size, and the mismatch reads as a bug.
- No decorative icon without aria-hidden. If the icon repeats or embellishes visible text, mark it
aria-hidden="true"so screen readers do not announce it twice. - No icon-only control without aria-label. When the icon is the only content, the accessible name must come from
aria-label(see the icon-only buttons above). - No new families in registry code. Registry components import from
lucide-reactonly; icon slots stayReact.ReactNodeso consumers can pass their own family.