# Using icons

URL: /docs/plugins/payload-icons/using-icons

Drop `<Icon name>` into any server component to inline a recolorable SVG, or call getIconSvg to render one in your own markup.

## `<Icon>`

`<Icon name="…" />` is the frontend surface. It resolves `name` through the **active set** and
inlines the matched icon as a real `<svg>`. Your `className` and props win over the SVG's intrinsic
attributes, and the glyph inherits CSS `color` through `currentColor`.

Build the component once from the `createIcon` factory — passing it a Payload handle — then import
that wherever you render icons:

```tsx title="src/components/Icon.ts"
import config from '@payload-config'
import { getPayload } from 'payload'
import { createIcon } from '@pro-laico/payload-icons/components/Icon'

export const Icon = createIcon(getPayload({ config }))
```

```tsx
import { Icon } from '@/components/Icon'

<Icon name="arrow-right" className="size-6 text-primary" />
```

> It's an async **server component** (it queries Payload), so render it in a server component / page.
> You pass the Payload handle when you build the component with `createIcon(getPayload({ config }))` —
> the package never self-resolves your config. When `name` isn't in the active set it renders the
> `fallback` you pass (or a small built-in warning glyph), never nothing.

> **Accessibility.** `<Icon>` renders `aria-hidden` by default, which is right for decorative icons. When the
> icon carries meaning (e.g. it's the only content of a button), pass `aria-hidden={false}` plus an
> `aria-label` or a `<title>`; your props always win over the defaults.

## Props

**Reference**

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` |  | The name to render, matched against each entry's name in the active iconSet's iconsArray. Resolved server-side through the active set. _(required)_ |
| `fallback` | `string` | `built-in warning glyph` | Optional SVG string rendered when name doesn't match any icon in the active set. Omit it to use the built-in warning glyph. |
| `draft` | `boolean` | `false` | Resolve through the draft icon set. A prop rather than a draftMode() call, so published pages stay static — a preview route passes draft={(await draftMode()).isEnabled} explicitly. |
| `...svgProps` | `SVGAttributes` |  | Any SVG attribute (className, style, width, …) spread onto the rendered svg, winning over the source's intrinsic attributes. |

**TypeScript**

```tsx
import { Icon } from '@/components/Icon' // built with createIcon(getPayload({ config }))

// `name` is the only required prop; it resolves through the active set.
<Icon
  name="arrow-right"
  className="size-6 text-primary"
  // fallback={myCustomSvgString}  // shown if the name isn't in the active set
/>
```

## Previewing draft icons

`<Icon>` reads the published lane. That's a deliberate default: reading `draftMode()` on every icon
would make every page that renders one dynamic, and a page of icons could never be prerendered. A
preview route opts in per icon:

```tsx
import { draftMode } from 'next/headers'

const { isEnabled } = await draftMode()

<Icon name="arrow-right" draft={isEnabled} />
```

The two lanes cache separately, and publishing busts both — see [How it works](/docs/plugins/payload-icons/how-it-works).

## Rendering it yourself

`<Icon>` is just `getIconSvg` (from the `cache` subpath, server-only and cached) composed with the
pure `extractSvg*` helpers. Call `getIconSvg` yourself to render an icon in your own markup. One
query serves the whole active set, and it's cached across requests — a page calling it fifty times
is one read, and often none.

```
getIconSvg(payload: Payload | Promise<Payload>, name: string, draft = false): Promise<string | undefined>
```

Pass a live Payload handle as the first argument:

```tsx
import { getPayload } from 'payload'
import config from '@payload-config'
import { getIconSvg } from '@pro-laico/payload-icons/cache'
import { extractSvgContent, extractSvgProps } from '@pro-laico/payload-icons'

const svg = await getIconSvg(getPayload({ config }), 'arrow-right') // draft defaults to false (published)
if (!svg) return null

return (
  <svg
    {...extractSvgProps(svg)}
    className="size-6"
    dangerouslySetInnerHTML={{ __html: extractSvgContent(svg) }}
  />
)
```

It reads the active set's `_status: 'published'` rows on the published frontend (and the latest draft
in draft mode), so you never render an unpublished set.

## Styling with CVA + Tailwind

Because the optimizer rewrites fills to `currentColor` and `<Icon>` forwards `className` straight
onto the `<svg>`, one source SVG recolors and resizes from class names alone, with no custom wrapper
needed. Pair `<Icon>` with [`class-variance-authority`](https://cva.style) to turn `size` / `tone`
props into classes, one variant per line so the axes stay legible:

**Variants**

```tsx
import { cva } from 'class-variance-authority'

const iconClass = cva('inline-block shrink-0', {
  variants: {
    size: {
      xs: 'size-3.5',
      sm: 'size-4',
      base: 'size-5',
      lg: 'size-6',
      xl: 'size-8',
    },
    tone: {
      current: '',
      muted: 'text-muted-foreground',
      primary: 'text-primary',
      destructive: 'text-destructive',
    },
  },
  defaultVariants: { size: 'base', tone: 'current' },
})
```

**Usage**

```tsx
import { Icon } from '@/components/Icon' // built with createIcon(getPayload({ config }))

// One source SVG, every variant; the cva className recolors and resizes it.
<Icon name="star" className={iconClass({ size: 'sm' })} />
<Icon name="star" className={iconClass({ size: 'lg', tone: 'primary' })} />
<Icon name="star" className={iconClass({ size: 'xl', tone: 'destructive' })} />
```

The [`icons-sandbox` example](https://github.com/pro-laico/payload-plugins/tree/main/examples/icons-sandbox)
ships the full version: a presentational `Icon` (CVA over the inline `<svg>`) plus a name-based
`CmsIcon` server wrapper, along with a showcase page that renders one source SVG across the variant,
size, and tone axes.
