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

EcosystemIntermediate●6 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 (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 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.

The True TCO of Self-Hosting a Headless CMS
Ecosystem·12 min read

The True TCO of Self-Hosting a Headless CMS

Discover the real cost of self-hosting Strapi 5: infrastructure, DevOps labor, security, migrations, and hidden costs—with a practical TCO checklist.

·September 24, 2026
How Do You Preserve SEO Rankings During a CMS Migration?
Ecosystem·13 min read

How Do You Preserve SEO Rankings During a CMS Migration?

A developer's CMS migration SEO checklist: URL mapping, 301 redirects, on-page parity, robots.txt, redirect testing, and post-launch monitoring.

·September 24, 2026
What Is a Content Agent? Agentic AI for CMS Workflows Explained
Ecosystem·24 min read

What Is a Content Agent? Agentic AI for CMS Workflows Explained

Learn what a content agent is, how the observe-plan-act-evaluate loop works, and when agentic AI makes sense for your CMS workflows and content operations.

·September 24, 2026