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:
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
namestringrequiredThe 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 glyphOptional SVG string rendered when name doesn't match any icon in the active set. Omit it to use the built-in warning glyph.
draftbooleandefault falseResolve 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.
...svgPropsSVGAttributesAny 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.