# Icon

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

URL: https://apogee.avaui.dev/docs/icon
Last updated: 2026-10-15

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

Last updated October 15, 2026

```astro
<Icon icon={Camera} label="Take photo" />
<Icon icon={ChevronDown} />
```

## Add

```sh
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:

```astro
---
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`:

```astro
<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:

```astro
<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`:

```astro
<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:

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

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

```astro
<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:

```astro
<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):

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

```astro
<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`:

```astro
<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”).

## Related

[

IconToggleTwo icons in one box that swap with a scale and fade, driven by a ToggleButton or your own state.

](https://apogee.avaui.dev/docs/icon-toggle)[

ButtonTriggers an action. Renders a <button>, or an <a> when href is set. Hue, fill and height are three separate props.

](https://apogee.avaui.dev/docs/button)[

AvatarA user image, with an initials fallback derived from name.

](https://apogee.avaui.dev/docs/avatar)
