Atomic Payload
Plugins

@pro-laico/icons

Manage your SVG icons in the Payload admin: upload them, group them into reusable sets, and render any icon by name on your site.

@pro-laico/icons makes your SVG icons part of the CMS. Upload your own SVGs in the Payload admin and they're optimized and cleaned up automatically, ready for the frontend. Group them into named sets, mark one active, and render any icon by name on your site. It's as easy to use as a public icon library like Lucide, but the icons are yours, with no fixed catalog to live inside.

Installation

pnpm add @pro-laico/icons
npm install @pro-laico/icons
yarn add @pro-laico/icons

payload, next, react, @payloadcms/ui, and server-only are peers you already have in a Payload + Next.js app. svgo and svg-path-bbox are optional: they power the upload-time cleanup and viewBox tightening, so install them if you want SVGs optimized on upload.

Setup

Adding icons to your own Payload + Next.js project.

@pro-laico/core comes bundled with this plugin — it's a dependency, not a separate install. Its cached reads reach Payload through a config you register once in your Next.js instrumentation hook (registerPayloadConfig); see @pro-laico/core → Setup to wire it up.

Add the plugin to your Payload config

import { buildConfig } from 'payload'
import { iconsPlugin } from '@pro-laico/icons'

export default buildConfig({
  plugins: [iconsPlugin()],
})

This registers two collections: Icon (where you upload SVG files) and IconSet (where you group icons under a named set and mark one set active). To register only the upload collection and skip the grouping concept, pass includeIconSet: false.

Icon sets get live preview by default: iconSetOptions.livePreviewUrl defaults to @pro-laico/core's generateLivePreviewPath (built from PREVIEW_SECRET + NEXT_PUBLIC_SERVER_URL). Pass your own iconSetOptions.livePreviewUrl to override it or point preview elsewhere (see Options).

Regenerate the admin import map

The IconSet editor uses a custom row label component that Payload loads through its import map. After adding the plugin, regenerate the map so the admin can find it:

pnpm payload generate:importmap

Run this again whenever you add or remove plugins that contribute admin components.

Upload icons and build your sets in the admin

  1. Open the Icon collection and upload your SVG files. Each upload is cleaned up and its viewBox tightened automatically.
  2. Open the IconSet collection, create a named set (for example "Nav" or "Social"), add icons to it by name, and mark the set active. Only one set is active at a time, and that's the set your site renders from.

Render icons in your app

Render an icon by name from the active set with the <Icon> server component, imported from the /Icon subpath:

import { Icon } from '@pro-laico/icons/Icon'

export function Toolbar() {
  return (
    <nav>
      <Icon name="arrow-right" />
      <Icon name="arrow-right" className="size-6 text-primary" />
      <Icon name="logo" fallback={myCustomSvgString} />
    </nav>
  )
}

name looks the icon up in the active icon set; any JSX props you pass (className, style, width, …) are applied to the rendered <svg> and win over the source's intrinsic attributes; fallback is a raw SVG string used when no icon matches the name (otherwise a small warning glyph shows). Because lookups go through @pro-laico/core's cached getters, swapping the active set or editing one icon in the admin invalidates just the affected <svg>, not the whole page.

The atomic-payload template already registers the Icon and IconSet collections (with live preview wired up) and includes the import map. You just manage your icons and render them.

Upload and group your icons

Open the Icon collection and upload your SVG files, then open the IconSet collection, create a named set, add your icons to it, and mark the set active.

Place icons in content

The template registers the IconChild and SVGChild content blocks through @pro-laico/atomic's children system, so editors can drop an icon (picked from the active set) or paste raw SVG straight into a page's content, no code needed.

Render icons in code

To render an icon by name from your own components, use the bundled <Icon> server component:

import { Icon } from '@pro-laico/icons/Icon'

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

It resolves from the active icon set, so switching the active set in the admin restyles every icon your site renders by that name.

Using it in your app

The package's main frontend piece is the <Icon> server component at the @pro-laico/icons/Icon subpath. It looks an icon up by name in the currently active icon set and inlines it as an <svg>. Render it from inside a server component, the way the package's own block renders it:

import { Icon } from '@pro-laico/icons/Icon'

// A real server component: pass each icon's name from the active set.
export function SiteFooter() {
  return (
    <footer>
      <a href="https://github.com/your-org" aria-label="GitHub">
        <Icon name="github" className="size-6" />
      </a>
      <Icon name="logo" fallback={myCustomSvgString} />
    </footer>
  )
}

Because resolution is bound to the active IconSet, picking a different set in the admin re-routes every <Icon> to the new artwork without a code change. Keep icon names consistent across your sets so they stay interchangeable. If a name isn't found in the active set, the fallback SVG (or a built-in warning glyph) renders instead.

In a project that uses @pro-laico/atomic's children system, editors can also place icons in content through two blocks: IconChild (picks an icon by name from the active set) and SVGChild (pastes raw SVG inline). You add them to your children blocks list by their default block exports, and they render through @pro-laico/atomic/children:

// The children blocks your atomic config exposes to editors.
import { Icon as IconChildBlock } from '@pro-laico/icons/blocks/iconChild'
import { SVGBlock } from '@pro-laico/icons/blocks/svgChild'

const ChildBlocks = [IconChildBlock, SVGBlock /* …other children… */]

Need a className field (or other fields) on the block? Build a customized one with the createIconBlock / createSvgBlock factories instead of the default export, and spread your fields in via prependFields / appendFields.

Options

iconsPlugin(options?) accepts two top-level toggles plus two additive option bags, one for each collection. Your own hooks always run after the built-ins, so the SVG cleanup and cache-revalidation hooks always run first.

Prop

Type

The iconOptions and iconSetOptions options take their own nested keys:

All options at their defaults, as a working starting point:

import { generateLivePreviewPath } from '@pro-laico/core'

iconsPlugin({
  enabled: true,
  includeIconSet: true,
  iconOptions: {
    fields: [],
    // hooks and collection are unset
  },
  iconSetOptions: {
    // livePreviewUrl defaults to core's generateLivePreviewPath, so live preview
    // is already wired; pass your own only to override it.
    livePreviewUrl: ({ data, req }) => generateLivePreviewPath({ data, req }),
    extraSettingsFields: [],
    fields: [],
    iconRowFields: [],
    useAsTitle: 'title',
    group: 'Sets',
    // hooks is unset
  },
})

Caching & revalidation

The reads this package ships (@pro-laico/icons/cache) are server-side and cached, so a page resolves an icon without re-querying Payload on every render:

  • getCachedIconSet(draft): the active icon set's name + reference list.
  • getCachedIconByName(name, draft, iconSet): the SVG string for one name, resolved through that set.
  • getCachedIconOptions(draft, iconSet): the set as { label, value } options for the admin icon picker.

When you save or delete an Icon or IconSet, the collections' @pro-laico/core revalidation hooks bust the matching tags (iconSet, and each icon's own icon tag). Editing the active set re-routes lookups; editing one icon invalidates only that icon, so unrelated <svg> output stays cached. See Caching & revalidation for how the tags and withCache work.

Exports

Grouped by what each export is for.

The plugin

Export

Type

iconsPluginplugin
The plugin itself. Registers the Icon collection and, by default, the IconSet collection. Default and named export.

Parameters

options?:IconsPluginOptionsSee the Options table above. Every key is optional; IconSet live preview is wired by default, so pass iconSetOptions.livePreviewUrl only to override it, or includeIconSet: false to register the upload collection only.

Returns

PluginA Payload config plugin you add to buildConfig({ plugins: [...] }).

Example

import { buildConfig } from 'payload'import { iconsPlugin } from '@pro-laico/icons'import { generateLivePreviewPath } from '@pro-laico/core'export default buildConfig({plugins: [  // IconSet live preview is on by default; this shows overriding the URL builder.  iconsPlugin({    iconSetOptions: {      livePreviewUrl: ({ data, req }) => generateLivePreviewPath({ data, req }),    },  }),],})

Location

@pro-laico/icons
IconsPluginOptionstype
The TypeScript type for the plugin's options.

Location

@pro-laico/icons

Frontend components

Export

Type

Iconcomponent
Server component that renders an icon by name from the active set, with cached lookup and a fallback. The main way to put a CMS-managed icon on the frontend. Default and named export.

Parameters

name:stringThe icon name to look up in the active IconSet (each entry’s kebab-cased name).
fallback?:stringRaw SVG string rendered when name matches nothing in the active set. Defaults to a small inline warning glyph.
...svgProps?:React.SVGAttributes<SVGSVGElement>Any <svg> props (className, style, width, …). They ALWAYS win over the source SVG’s intrinsic attributes.

Returns

Promise<JSX.Element>An inlined <svg> (async server component).

Example

import { Icon } from '@pro-laico/icons/Icon'// A server component that renders icons from the active set by name:export function Toolbar() {return (  <nav>    <Icon name="arrow-right" />    <Icon name="arrow-right" className="size-6 text-primary" />    <Icon name="logo" fallback={myCustomSvgString} />  </nav>)}

Location

@pro-laico/icons/Icon
AtomicIconcomponent
A small, self-contained branding glyph (the Atomic Payload mark), tinted by node type. Not a CMS lookup: it takes no name and never reads an IconSet. Use it for admin/editor chrome that labels atomic node kinds, not for content icons. Default and named export.

Parameters

type:'tag' | 'form' | 'input' | 'button' | 'portal'Picks the fill/stroke color from the atomic distinct-color CSS variables (one per node kind).

Returns

JSX.ElementA fixed 24x24 <svg> glyph tinted for that node type.

Example

import { AtomicIcon } from '@pro-laico/icons/AtomicIcon'// Label an atomic node kind in editor chrome:export function NodeBadge() {return (  <span className="flex items-center gap-2">    <AtomicIcon type="button" />    Button  </span>)}

Location

@pro-laico/icons/AtomicIcon

SVG helpers

Export

Type

extractSvgContentfunction
Pulls the inner markup out of an SVG string (everything between the <svg> tags), so you can re-inline it on a fresh <svg> node. Returns the original string if it has no <svg> wrapper.

Parameters

svgString:stringA full SVG document string.

Returns

stringThe inner SVG markup (paths, groups, …) without the wrapper tags.

Example

import { extractSvgContent, extractSvgProps } from '@pro-laico/icons'// Render a stored SVG string as a native <svg>, the way IconChild does:function SvgFromString({ svg }: { svg: string }) {return <svg {...extractSvgProps(svg)} dangerouslySetInnerHTML={{ __html: extractSvgContent(svg) }} />}

Location

@pro-laico/icons
extractSvgPropsfunction
Pulls the attributes (viewBox, fill, xmlns, …) off an SVG’s root <svg> tag as a plain props object you can spread onto a JSX <svg>. Handles namespaced and hyphenated attribute names.

Parameters

svgString:stringA full SVG document string.

Returns

Record<string, string>The root tag’s attributes as { name: value }. Empty object when there is no <svg> tag.

Example

import { extractSvgContent, extractSvgProps } from '@pro-laico/icons'const source = icon.svgString// JSX props you add after the spread win over the source's intrinsic attrs:return <svg {...extractSvgProps(source)} className="size-6" dangerouslySetInnerHTML={{ __html: extractSvgContent(source) }} />

Location

@pro-laico/icons

Content blocks

Export

Type

createIconBlockfunction
Factory that builds the IconChild content block (an editor picks an icon by name from the active set). prependFields / appendFields are spread at the start / end of the Icon tab, so the consumer decides what goes there and the block carries no CSS dependency of its own.

Parameters

options?:IconBlockOptions{ prependFields?, appendFields? }: extra fields to spread before / after the icon picker on the Icon tab.

Returns

BlockA Payload Block you add to a blocks field (e.g. the atomic children blocks list).

Example

import { createIconBlock, Icon as IconChildBlock } from '@pro-laico/icons/blocks/iconChild'import { ClassNameField } from '@pro-laico/styles/fields/className'// Default block, or build one that carries a className field:const StyledIconBlock = createIconBlock({ prependFields: [ClassNameField({ namePrefix: 'icon' })] })// Expose it on whatever blocks field hosts children:{ type: 'blocks', name: 'children', blocks: [IconChildBlock, StyledIconBlock] }

Location

@pro-laico/icons/blocks/iconChild
Iconobject
The default IconChild block, equivalent to createIconBlock() with no extra fields. Import this when you do not need to extend it.

Location

@pro-laico/icons/blocks/iconChild
IconBlockOptionstype
The options type for createIconBlock (prependFields / appendFields).

Location

@pro-laico/icons/blocks/iconChild
IconChildcomponent
Server component that renders a saved IconChild block: resolves the stored icon name through the active set with the same cached lookup + warning fallback as <Icon>. Rendered for you by @pro-laico/atomic’s children renderer.

Parameters

block:RenderChild<IconChild>The block data and pass-through props supplied by the atomic children renderer.

Returns

Promise<JSX.Element>The resolved icon as an inlined <svg> (async server component).

Example

import { IconChild } from '@pro-laico/icons/blocks/iconChild/component'// The atomic children renderer maps the IconChild block slug to this component.// You rarely render it by hand, but it is an async server component:<IconChild block={childData} pt={passThroughProps} />

Location

@pro-laico/icons/blocks/iconChild/component
createSvgBlockfunction
Factory that builds the SVGChild content block (an editor pastes raw SVG: viewBox, fill, and the path data). prependFields / appendFields are spread at the start / end of the Content tab, so the block carries no CSS dependency of its own.

Parameters

options?:SvgBlockOptions{ prependFields?, appendFields? }: extra fields to spread before / after the SVG inputs on the Content tab.

Returns

BlockA Payload Block you add to a blocks field.

Example

import { createSvgBlock, SVGBlock } from '@pro-laico/icons/blocks/svgChild'// Default block, or one with extra fields:const block = createSvgBlock()// Expose it on a children blocks field next to IconChild:{ type: 'blocks', name: 'children', blocks: [SVGBlock] }

Location

@pro-laico/icons/blocks/svgChild
SVGBlockobject
The default SVGChild block, equivalent to createSvgBlock() with no extra fields.

Location

@pro-laico/icons/blocks/svgChild
SvgBlockOptionstype
The options type for createSvgBlock (prependFields / appendFields).

Location

@pro-laico/icons/blocks/svgChild
SVGChildcomponent
Server component that renders a saved SVGChild block: inlines the pasted SVG attributes and contents onto a native <svg>. Rendered for you by @pro-laico/atomic’s children renderer.

Parameters

block:RenderChild<SVGChild>The block data and pass-through props supplied by the atomic children renderer.

Returns

JSX.ElementThe pasted SVG as an inlined <svg>.

Example

import { SVGChild } from '@pro-laico/icons/blocks/svgChild/component'// Mapped to the SVGChild block slug by the atomic children renderer:<SVGChild block={childData} pt={passThroughProps} />

Location

@pro-laico/icons/blocks/svgChild/component

Cache getters

Export

Type

getCachedIconSetfunction
Cached read of the active icon set’s entries (each name plus its icon reference id). Wrapped so a page reads it once per request and revalidated when an IconSet is saved or deleted. Resolve it first, then pass it to the two getters below.

Parameters

draft:booleanRead the draft (true) or published (false) variant.

Returns

Promise<IconSetReturn>{ iconsArray: { name, icon }[] } for the active set.

Example

import { draftMode } from 'next/headers'import { getCachedIconByName, getCachedIconSet } from '@pro-laico/icons/cache'// Resolve the active set once, then look icons up by name against it:const { isEnabled: draft } = await draftMode()const iconSet = await getCachedIconSet(draft)const svg = await getCachedIconByName('check', draft, iconSet)

Location

@pro-laico/icons/cache
getCachedIconByNamefunction
Cached read of one icon’s SVG string, resolved by name through the active set. This is what <Icon> calls internally. Each icon sits behind its own icon tag, so editing one icon invalidates only that icon, not the whole page.

Parameters

name:stringThe icon name to resolve in the active set.
draft:booleanDraft or published variant.
iconSet:IconSetReturn | undefinedThe active set from getCachedIconSet(draft).

Returns

Promise<string | undefined>The icon’s SVG string, or undefined when the name matches nothing.

Example

import { draftMode } from 'next/headers'import { extractSvgContent, extractSvgProps } from '@pro-laico/icons'import { getCachedIconByName, getCachedIconSet } from '@pro-laico/icons/cache'// A hand-rolled icon-by-name server component (what <Icon> wraps):async function IconByName({ name }: { name: string }) {const { isEnabled: draft } = await draftMode()const iconSet = await getCachedIconSet(draft)const svg = await getCachedIconByName(name, draft, iconSet)if (!svg) return nullreturn <svg {...extractSvgProps(svg)} dangerouslySetInnerHTML={{ __html: extractSvgContent(svg) }} />}

Location

@pro-laico/icons/cache
getCachedIconOptionsfunction
Cached read of the active set as { label, value } options for an admin select. The bundled IconSelect field uses it so the picker stays in sync with the active set.

Parameters

draft:booleanDraft or published variant.
iconSet:IconSetReturn | undefinedThe active set from getCachedIconSet(draft).

Returns

Promise<{ label: string; value: string }[]>One option per icon in the active set, value = name.

Example

import { getCachedIconOptions, getCachedIconSet } from '@pro-laico/icons/cache'// In an admin server field, build the icon picker's options from the active set:const iconSet = await getCachedIconSet(true)const options = await getCachedIconOptions(true, iconSet)

Location

@pro-laico/icons/cache

Admin

Export

Type

IconSelectcomponent
Admin server field that renders a select whose options are the active icon set’s icons (via getCachedIconOptions). Wire it as a field’s Field component to let editors pick an icon by name. Default and named export.

Parameters

...props:SelectFieldServerComponent propsThe clientField, path, schemaPath, and permissions Payload passes to a server field component.

Returns

Promise<JSX.Element>A Payload SelectField populated with the active set’s icon names.

Example

// In a field config, render the icon picker as that field's admin component:{name: 'icon',type: 'text',admin: { components: { Field: { path: '@pro-laico/icons/admin/iconSelect' } } },}

Location

@pro-laico/icons/admin/iconSelect
IconLabelPathconstant
The admin import-map path for the IconSet row label component. Re-export it from your ui barrel so the generated import map can resolve it.

Location

@pro-laico/icons

Types

Export

Type

Icon (type)type
TypeScript type for typed access to your Icon documents.

Location

@pro-laico/icons/schema
IconSet (type)type
TypeScript type for typed access to your IconSet documents.

Location

@pro-laico/icons/schema
IconSetReturntype
The shape getCachedIconSet returns: { iconsArray: { name, icon }[] }.

Location

@pro-laico/icons/cache

On this page