A library-agnostic icon wrapper — forwards a Lucide icon's props, or accepts a raw SVG slot, with built-in accessibility.

Last updated

Show 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 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 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:

Camera off
Show code
<span class="text-critical"><Icon icon={Camera} label="Camera" /> Camera off</span>

Sizes with Tailwind classes, in step with the type scale:

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

PaidHomeSettings
Show 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 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; renders role="img" + aria-label.
  • Decorative icon (sits next to visible text) → omit label; sets aria-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 label to any icon that stands alone (icon-only buttons, status indicators).
  • Do omit label for 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/size inconsistently across an interface — pick one weight and stick to it.

Content

  • label names the action or meaning (“Take photo”), not the icon itself (“camera icon”).