Icon
A library-agnostic icon wrapper — forwards a Lucide icon's props, or accepts a raw SVG slot, with built-in accessibility.
Last updated
Show codeHide code
<Icon icon={Camera} label="Take photo" />
<Icon icon={ChevronDown} />Add
npx @ava-ui/apogee add icon
No Apogee dependencies. Recommends @lucide/astro as the default icon set —
bring your own via the raw SVG slot if you prefer a different one.
Usage
With a Lucide (or Lucide-shaped) icon component, via the icon prop:
---
import Icon from '@/components/apogee/Icon/Icon.astro'
import { Camera } from '@lucide/astro'
---
<Icon icon={Camera} label="Take photo" />
With any other icon set, pass a raw <svg> as the default slot instead — omit
icon:
<Icon label="Star this repo">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3 7h7l-5.5 4.5L18 21l-6-4-6 4 1.5-7.5L2 9h7z" /></svg>
</Icon>
API
@composable — exposed to Content Editors in AVA. Any other Lucide prop
(fill, stroke-linecap, …) or attribute passes through, to whichever
element renders — the icon component, or the wrapping <span> for the raw
SVG slot.
| Prop | Type | Default | Description |
|---|---|---|---|
icon |
any — a Lucide icon component |
— | The icon to render. Omit to render a raw <svg> slot instead. |
label |
string |
— | Accessible label. Sets role="img" + aria-label. Omit for decorative icons (sets aria-hidden="true" instead). |
size |
number | string |
— (Lucide’s default) | Icon size in px. Prefer a Tailwind size-* class for consistency with surrounding text. |
strokeWidth |
number | string |
— (Lucide’s default) | Stroke width. |
absoluteStrokeWidth |
boolean |
— (Lucide’s default) | Keep stroke width constant regardless of size. |
color |
token name or any colour string | — | A token (primary, neutral, neutral-faded, critical, positive, warning, inherit) maps to its text colour; anything else passes to the SVG. |
autoWidth |
boolean |
false |
Keep the glyph’s own aspect ratio instead of a square box. |
class |
string |
— | Extra classes, merged last (shrink-0 is always applied). |
Variants
The two render modes — a Lucide icon via icon, or a raw SVG via the slot —
render identically once styled:
Show codeHide code
<Icon icon={ChevronDown} class="text-primary" />
<Icon class="text-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6" /></svg>
</Icon>Colours
Every token color:
Show codeHide code
<Icon icon={Camera} color="primary" class="size-6" />
<Icon icon={Camera} color="neutral" class="size-6" />
<Icon icon={Camera} color="neutral-faded" class="size-6" />
<Icon icon={Camera} color="critical" class="size-6" />
<Icon icon={Camera} color="positive" class="size-6" />
<Icon icon={Camera} color="warning" class="size-6" />
<Icon icon={Camera} color="inherit" class="size-6" />States
Static: no interactive states. It takes currentColor, so it follows the hover or disabled colour of the control around it.
Examples
Inherits currentColor, so it picks up surrounding text colour:
Show codeHide code
<span class="text-critical"><Icon icon={Camera} label="Camera" /> Camera off</span>Sizes with Tailwind classes, in step with the type scale:
Show codeHide code
<Icon icon={Star} class="size-3.5" />
<Icon icon={Star} class="size-4" />
<Icon icon={Star} class="size-5" />
<Icon icon={Star} class="size-6" />
<Icon icon={Star} class="size-8" />Stroke width, and a constant stroke as the icon grows:
Show codeHide code
<Icon icon={Heart} strokeWidth={1} class="size-8" />
<Icon icon={Heart} strokeWidth={1.5} class="size-8" />
<Icon icon={Heart} strokeWidth={2} class="size-8" />
<Icon icon={Heart} strokeWidth={2.5} class="size-8" />
<Icon icon={Heart} absoluteStrokeWidth class="size-12" />Filled: fill="currentColor" for a solid glyph, or a tint of the colour for
a selected state (the docs sidebar marks the current page this way):
Show codeHide code
<Icon icon={Bookmark} class="size-6" />
<Icon icon={Bookmark} class="size-6 fill-current/10" />
<Icon icon={Bookmark} class="size-6" fill="currentColor" />
<Icon icon={Heart} color="critical" class="size-6" fill="currentColor" />In other components: buttons, a badge, a featured tile, a navigation link.
Show codeHide code
<Button><Plus slot="start" />New</Button>
<Button variant="outline" color="neutral">Download<Download slot="end" /></Button>
<Button iconOnly variant="ghost" color="critical" aria-label="Delete"><Trash2 /></Button>
<Badge color="positive"><Check slot="start" />Paid</Badge>
<FeaturedIcon color="primary"><Bell /></FeaturedIcon>
<NavLink href="#" active><Home slot="start" />Home</NavLink>Directional icons mirror in right-to-left layouts with rtl:-scale-x-100:
Show codeHide code
<Button variant="link">Next<ArrowRight slot="end" class="rtl:-scale-x-100" /></Button>Accessibility
- Meaningful icon (conveys information on its own — e.g. inside an icon-only
button) → pass
label; rendersrole="img"+aria-label. - Decorative icon (sits next to visible text) → omit
label; setsaria-hidden="true"so assistive tech skips it. - The raw-SVG slot gets the same treatment via its wrapping
<span>— the inner<svg>itself needs no extra ARIA. - No JS.
Tokens
None directly. Icon renders currentColor by default — set its colour with
a Tailwind text-* utility (which reads the same tokens as the rest of
Apogee) or the color prop, rather than a fixed value.
Do / Don’t
- Do give a
labelto any icon that stands alone (icon-only buttons, status indicators). - Do omit
labelfor icons next to visible text — otherwise screen readers announce the icon and the text twice. - Don’t hand-roll
<svg>paths for Apogee’s own UI chrome — use Lucide for that; the raw slot is for consumers’ own icon sets. - Don’t mix
strokeWidth/sizeinconsistently across an interface — pick one weight and stick to it.
Content
labelnames the action or meaning (“Take photo”), not the icon itself (“camera icon”).