@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/iconsnpm install @pro-laico/iconsyarn add @pro-laico/iconspayload, 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:importmapRun this again whenever you add or remove plugins that contribute admin components.
Upload icons and build your sets in the admin
- Open the Icon collection and upload your SVG files. Each upload is cleaned up and its
viewBoxtightened automatically. - 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'sname+ 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
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/iconsIconsPluginOptionstype
Location
@pro-laico/iconsFrontend components
Export
Type
Iconcomponent
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/IconAtomicIconcomponent
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/AtomicIconSVG helpers
Export
Type
extractSvgContentfunction
<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/iconsextractSvgPropsfunction
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/iconsContent blocks
Export
Type
createIconBlockfunction
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/iconChildIconobject
IconChild block, equivalent to createIconBlock() with no extra fields. Import this when you do not need to extend it.Location
@pro-laico/icons/blocks/iconChildIconBlockOptionstype
createIconBlock (prependFields / appendFields).Location
@pro-laico/icons/blocks/iconChildIconChildcomponent
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/componentcreateSvgBlockfunction
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/svgChildSVGBlockobject
SVGChild block, equivalent to createSvgBlock() with no extra fields.Location
@pro-laico/icons/blocks/svgChildSvgBlockOptionstype
createSvgBlock (prependFields / appendFields).Location
@pro-laico/icons/blocks/svgChildSVGChildcomponent
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/componentCache getters
Export
Type
getCachedIconSetfunction
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/cachegetCachedIconByNamefunction
<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/cachegetCachedIconOptionsfunction
{ 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/cacheAdmin
Export
Type
IconSelectcomponent
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/iconSelectIconLabelPathconstant
ui barrel so the generated import map can resolve it.Location
@pro-laico/iconsTypes
Export
Type
Icon (type)type
Location
@pro-laico/icons/schemaIconSet (type)type
Location
@pro-laico/icons/schemaIconSetReturntype
getCachedIconSet returns: { iconsArray: { name, icon }[] }.Location
@pro-laico/icons/cacheRelated
@pro-laico/styles
Manage your whole design system from the Payload admin: write Tailwind on your blocks and author colors, tokens, reusable shortcuts, and swappable design sets as content, with no config file to edit and no redeploy to restyle your site.
@pro-laico/fonts
Manage custom fonts in the Payload admin and use them with next/font/local: upload your fonts, pick the active ones, and a build step delivers them to your app.