Plugins
FieldsIcon

Adapters

First-party Lucide, Radix, and Tabler adapters, and the contract for defining your own.

An adapter connects one icon library to the field: a lazy manifest (names, tags, categories) for browsing and validation, and client components that render icons in the admin.

First-party adapters

import { lucideAdapter } from '@10x-media/fields/icon/adapters/lucide'
import { radixAdapter } from '@10x-media/fields/icon/adapters/radix'
import { tablerAdapter } from '@10x-media/fields/icon/adapters/tabler'

Each adapter has an optional peer dependency on its icon package (lucide-react, @radix-ui/react-icons, @tabler/icons-react); install the ones you use. Manifests are generated from the libraries' own metadata at this package's build time and ship as lazily imported modules, so registering an adapter adds nothing to any eager bundle.

Register adapters globally through the plugin, or per field:

fields({ icon: { adapters: [lucideAdapter(), tablerAdapter()] } })

iconField({ name: 'icon', adapters: [radixAdapter()] })  // this field: Radix only

Defining an adapter

defineIconAdapter pins the adapter contract version; you provide the rest:

import { defineIconAdapter } from '@10x-media/fields/icon'

export const acmeIcons = defineIconAdapter({
  slug: 'acme',
  label: 'Acme Icons',
  loadManifest: () => import('./generated/manifest').then((m) => m.manifest),
  Icon: '@acme/payload-icons/client#AcmeAdapterIcon',
  Assets: '@acme/payload-icons/client#AcmeAdapterAssets',
})
  • slug becomes the value prefix (acme:rocket) and must stay stable forever; it is stored data.
  • loadManifest is called on first drawer open, never eagerly, and resolves the generated manifest. Its shape is { icons: [{ name, tags, categories, label? }], categories: string[] }.
  • label on an icon is optional and replaces the label otherwise derived from its name. Supply it wherever a name is a code rather than words: HUN reads as Hun without one, and a screen reader announces exactly that. It accepts a per-locale record as well as a string. Every manifest without one is unaffected.
  • resolveMeta(name, ctx) is optional and answers one name exactly, serving both validation and label resolution. Supply it for a library whose contents change at runtime; it stays correct where a cached manifest snapshot cannot. See layers.
  • Icon and Assets are import-map component paths (the same string-path convention as every admin component). Icon renders one icon by name, lazily; Assets loads the manifest for the drawer. An optional Nodes path supplies bulk glyph data for fast inline-SVG drawer rendering; omit it for libraries the drawer renders through the per-icon Icon component (Radix does). The first-party adapters are the reference implementation for these components.

Labels an editor sees

Where a library supplies label, the drawer deliberately shows two different things. The accessible name carries the label alone, so a screen reader browsing a country library announces "Hungary" rather than "Hungary HUN" across every cell. The tooltip shows the label with the raw name beside it, because that tooltip is the only place an editor can discover that "Hungary" stores as flag:HUN, which they need in order to write frontend code against the value.

Hand-writing a manifest for a large library is busywork; codegen generates the manifest, and the per-icon import map those components load, from an icon set you describe.

On this page