Payload Plugins
Pluginspayload-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.

For AI / LLMs: View Markdown

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

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 }))
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

namestringrequired

The name to render, matched against each entry's name in the active iconSet's iconsArray. Resolved server-side through the active set.

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

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

...svgPropsSVGAttributes

Any SVG attribute (className, style, width, …) spread onto the rendered svg, winning over the source's intrinsic attributes.

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:

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.

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:

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 to turn size / tone props into classes, one variant per line so the axes stay legible:

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' },
})
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 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.

On this page