✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

EcosystemIntermediate6 min read

Finding and using Strapi design system icons in your plugin

September 9, 2026
Finding and using Strapi design system icons in your plugin

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'))" | less

Or 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 (AppsGridNine, EnvelopMail, RefreshArrowClockwise, PictureImage, PuzzlePuzzlePiece, DashboardSquaresFour, TwitterX), 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 Magic or Rocket than by Cog.
  • Avoid icons Strapi already uses for core navigation, since a duplicate Layer or Feather in 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.

Further reading

Theodore Kelechukwu OnyejiakuDevRel and Community | Software Developer | Technical Writer

Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.

Seeding Data in Strapi: Why Migrations are the Wrong Tool
EcosystemIntermediate·7 min read

Seeding Data in Strapi: Why Migrations are the Wrong Tool

Migrations run before Strapi's schema sync, so seeding a brand-new content type fails on the missing table. Use bootstrap instead, idempotently.

·September 9, 2026
Strapi and TypeScript: compiling when there are type errors
EcosystemAdvanced·6 min read

Strapi and TypeScript: compiling when there are type errors

A single type error stops your Strapi dev server. The tsconfig settings that let it build anyway, and when each one is actually appropriate.

·September 9, 2026
"Cannot read properties of undefined (reading 'attributes')" in Strapi
EcosystemIntermediate·6 min read

"Cannot read properties of undefined (reading 'attributes')" in Strapi

Strapi will not boot because a content type references a component or relation that does not exist. How to find the broken reference quickly.

·September 9, 2026