You are building a Strapi plugin and need an icon for the left-hand navigation entry, a settings section, a button, or a custom field. You open node_modules/@strapi/icons/ and find hundreds of compiled JavaScript files with names like Feather.js and PuzzlePiece.js, and no way to see what any of them look like.
Here are the ways to browse them, and how to use them once you have picked one.
Browsing the icons
The Strapi Design System documentation
The Strapi Design System site is the canonical reference. Its icons page renders every icon with its component name, which is exactly what you need to copy into an import.
Search there first, because it is maintained alongside the package, so it reflects the version you are likely installing.
Community icon viewers
The community has built standalone icon browsers over the years, typically deployed as a small static site listing every export from @strapi/icons with click-to-copy names. They are convenient, and the caveat is the usual one for community tooling: an unmaintained viewer pins whichever package version it was built against, so an icon it shows may not exist in your installed version, and icons added since will be missing.
Generate your own index: always accurate
The most reliable option, and it takes a minute. Because @strapi/icons exports every icon as a named React component, you can enumerate them from your own node_modules and render the exact set your project has.
List the names:
node -e "console.log(Object.keys(require('@strapi/icons')).sort().join('\n'))" | lessOr drop a scratch page into your plugin's admin code that renders all of them:
import * as Icons from '@strapi/icons';
import { Box, Flex, Typography } from '@strapi/design-system';
const IconGallery = () => {
const entries = Object.entries(Icons).filter(
([, value]) => typeof value === 'function' || typeof value === 'object'
);
return (
<Box padding={8}>
<Typography variant="alpha">{entries.length} icons</Typography>
<Flex wrap="wrap" gap={4} paddingTop={6}>
{entries.map(([name, Icon]) => (
<Flex
key={name}
direction="column"
alignItems="center"
gap={2}
width="120px"
padding={3}
onClick={() => navigator.clipboard.writeText(name)}
cursor="pointer"
>
{/* @ts-expect-error dynamic icon component */}
<Icon width="2rem" height="2rem" />
<Typography variant="pi" textAlign="center">{name}</Typography>
</Flex>
))}
</Flex>
</Box>
);
};
export default IconGallery;Register it as a temporary plugin page, browse it, click to copy the name, and delete the file. It cannot go out of date, because it reads the package you actually installed.
Using an icon
In the left-hand navigation
The plugin menu link takes an icon component:
import { PuzzlePiece } from '@strapi/icons';
import { PLUGIN_ID } from './pluginId';
export default {
register(app) {
app.addMenuLink({
to: `/plugins/${PLUGIN_ID}`,
icon: PuzzlePiece,
intlLabel: {
id: `${PLUGIN_ID}.plugin.name`,
defaultMessage: 'My Plugin',
},
Component: async () => {
const { App } = await import('./pages/App');
return App;
},
permissions: [],
});
app.registerPlugin({
id: PLUGIN_ID,
name: PLUGIN_ID,
});
},
};Pass the component itself, not an element: icon: PuzzlePiece, not icon: <PuzzlePiece />. Strapi renders it and controls the sizing. Passing an element is the single most common mistake here and produces a menu entry with no icon and no error.
In a settings section
app.createSettingSection(
{
id: PLUGIN_ID,
intlLabel: { id: `${PLUGIN_ID}.section`, defaultMessage: 'My Plugin' },
},
[
{
intlLabel: { id: `${PLUGIN_ID}.settings`, defaultMessage: 'Configuration' },
id: 'settings',
to: `/settings/${PLUGIN_ID}`,
Component: async () => {
const { Settings } = await import('./pages/Settings');
return Settings;
},
permissions: [],
},
]
);Inside components
import { Check, Trash, Pencil } from '@strapi/icons';
import { Button, IconButton } from '@strapi/design-system';
const Toolbar = () => (
<>
<Button startIcon={<Check />}>Save</Button>
<IconButton label="Edit" onClick={handleEdit}>
<Pencil />
</IconButton>
<IconButton label="Delete" variant="danger" onClick={handleDelete}>
<Trash />
</IconButton>
</>
);Inside a component you do render an element: <Check />. The difference from addMenuLink catches people out; the registration APIs take a component reference, JSX props take elements.
For a custom field
app.customFields.register({
name: 'color-picker',
pluginId: PLUGIN_ID,
type: 'string',
icon: Paint,
intlLabel: { id: `${PLUGIN_ID}.color-picker.label`, defaultMessage: 'Color' },
intlDescription: { id: `${PLUGIN_ID}.color-picker.description`, defaultMessage: 'Pick a color' },
components: {
Input: async () => import('./components/ColorPickerInput'),
},
});Sizing and colour
Icons are SVGs, so they inherit currentColor and respond to explicit dimensions:
import { Check } from '@strapi/icons';
import { Box } from '@strapi/design-system';
// Explicit size
<Check width="1.5rem" height="1.5rem" />
// Inherit colour from a parent
<Box color="success600">
<Check />
</Box>
// Design system colour token
<Check fill="danger600" />Prefer design system colour tokens (success600, danger600, neutral500) over hex values. They are theme-aware, so your plugin follows the admin panel's light and dark modes without extra work.
Practical notes
Icons live in a separate package from components. @strapi/design-system provides Button, Box, Flex, Typography; @strapi/icons provides the icons. Importing an icon from the design system package fails.
There are two entry points, not one. @strapi/icons holds the general-purpose icons: monochrome, fill defaults to currentColor, viewBox is 0 0 32 32, so they scale to the parent and inherit its colour. @strapi/icons/symbols holds the symbols: brand marks (Strapi, Github, Discord, Medium, X), field-type glyphs (TextField, BooleanField, MediaField, RelationField, DynamicZoneField…), and empty-state illustrations. Symbols have baked-in colours, so set only width and height on them. node -e against the root package will not list any of these; enumerate @strapi/icons/symbols separately.
Names changed in design-system v2, and this is the most likely cause of an import that used to work. A large batch of icons was renamed (Apps → GridNine, Envelop → Mail, Refresh → ArrowClockwise, Picture → Image, Puzzle → PuzzlePiece, Dashboard → SquaresFour, Twitter → X), and another batch moved out to @strapi/icons/symbols. If you are porting a v4-era plugin, work through the v1 → v2 migration guide rather than guessing.
Both are peer dependencies of your plugin. Declare them in peerDependencies, not dependencies, so your plugin uses the host application's copy rather than bundling a second one. Two React trees in one admin panel is a class of bug you do not want.
{
"peerDependencies": {
"@strapi/design-system": "^2.0.0",
"@strapi/icons": "^2.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}Icon names change between major versions. The set was reorganised in the Strapi 5 design system release. If you are porting a v4 plugin and an import suddenly resolves to undefined, regenerate your index with the node -e snippet above and look for the renamed equivalent.
A missing icon fails quietly. An undefined component in addMenuLink renders nothing rather than throwing. If your menu entry has no icon, check the import name before anything else.
Choosing well
- Pick something that describes the domain, not the mechanism. A SEO plugin is better served by
MagicorRocketthan byCog. - Avoid icons Strapi already uses for core navigation, since a duplicate
LayerorFeatherin the sidebar makes the panel harder to scan. - Use the same icon in the nav, the settings section, and your empty states, so the plugin reads as one thing.



