How to Add Custom Icons to Lucide — The Complete 2026 Guide
Learn how to extend Lucide React with your own custom SVG icons using the official createLucideIcon factory, wrapper components, and design-matching techniques.
Lucide ships 1,500+ icons, but every product eventually needs one it doesn’t have. A brand logo, an industry-specific glyph, a bespoke UI symbol. The question is never whether to add custom icons, it’s how to do it without breaking the consistent feel Lucide is known for.
This guide walks through every approach, from a quick one-off wrapper to a full custom icon package that integrates seamlessly with your existing Lucide usage.
Why Lucide’s consistency matters
Before adding anything custom, it helps to understand what makes Lucide icons look cohesive. Every icon is drawn on a 24x24px grid with a 2px stroke, rounded line caps, and rounded line joins. The visual weight is uniform because the rules are strict.
When you add a custom icon without following these rules, it will visually clash, especially at small sizes where stroke weight differences become obvious. The fix is to design (or optimize) your SVG to match these constraints before writing a single line of React.
The createLucideIcon factory
Lucide exposes a first-class factory function for creating custom icons that are indistinguishable from built-in ones. This is the right tool for almost every use case.
npm install lucide-react
The factory signature:
import { createLucideIcon } from 'lucide-react';
const IconName = createLucideIcon('IconName', [
// SVG path data tuples: [element, { attributes }]
]);
Each tuple in the array maps to an SVG child element. For most icons, you’ll have one or more path elements.
Full example: a custom “Sparkles Stack” icon
Say you need a stacked-sparkles icon that doesn’t exist in Lucide. Here’s the complete pattern:
import { createLucideIcon } from 'lucide-react';
const SparklesStack = createLucideIcon('SparklesStack', [
['path', { d: 'M12 3 L13.5 7.5 L18 9 L13.5 10.5 L12 15 L10.5 10.5 L6 9 L10.5 7.5 Z', key: 'star1' }],
['path', { d: 'M18 14 L18.75 16.25 L21 17 L18.75 17.75 L18 20 L17.25 17.75 L15 17 L17.25 16.25 Z', key: 'star2' }],
['path', { d: 'M6 14 L6.75 16.25 L9 17 L6.75 17.75 L6 20 L5.25 17.75 L3 17 L5.25 16.25 Z', key: 'star3' }],
]);
export default SparklesStack;
Use it exactly like any other Lucide icon:
<SparklesStack size={24} strokeWidth={2} className="text-yellow-500" />
Because createLucideIcon wraps your paths in Lucide’s standard <svg> wrapper, all standard props work automatically: size, color, strokeWidth, className, absoluteStrokeWidth, and all SVG event handlers.
Preparing your SVG for Lucide
Raw SVGs from Figma, Illustrator, or icon sites are rarely Lucide-ready. Run through this checklist before converting:
| Check | What to fix |
|---|---|
ViewBox is 0 0 24 24 | Rescale or re-export at 24px |
No hardcoded fill or stroke colors | Remove or set to currentColor |
| Stroke width is 2px | Normalize all strokes to 2px |
Line caps and joins are round | Change in your editor before export |
No <g> wrapper groups | Flatten the SVG structure |
| No unnecessary metadata | Run through SVGO |
Optimizing with SVGO
Run your SVG through SVGO before extracting the path data:
npx svgo --input icon.svg --output icon.optimized.svg \
--config '{"plugins":["preset-default",{"name":"removeViewBox","active":false}]}'
Then open the output file and copy the d attribute values from each <path> element.
SVGO may preserve fill="none" and stroke="currentColor" on paths. Remove these from your path tuples in createLucideIcon because Lucide’s wrapper already applies them at the <svg> level. Keeping them can cause color inheritance to break in dark mode or when color prop is set.
Organizing custom icons at scale
For a handful of one-off icons, a single file works fine. For a design system or large product, you need a proper structure.
Recommended folder structure
src/
components/
icons/
index.ts ← re-exports everything
SparklesStack.tsx
CustomBrand.tsx
IndustryGlyph.tsx
Your index.ts barrel:
export { default as SparklesStack } from './SparklesStack';
export { default as CustomBrand } from './CustomBrand';
export { default as IndustryGlyph } from './IndustryGlyph';
Then import all your icons, both Lucide and custom, from a single place by re-exporting Lucide too:
// src/components/icons/index.ts
export * from 'lucide-react';
export { default as SparklesStack } from './SparklesStack';
export { default as CustomBrand } from './CustomBrand';
Now every icon in your app comes from one import path:
import { Search, SparklesStack, Bell } from '@/components/icons';
This pattern is fully tree-shakeable in Vite, Next.js, and Astro. The bundler only includes icons you actually import. Add "sideEffects": false to your package.json if you publish this as a package.
Using SVG files directly (alternative approach)
If you receive icons as .svg files and don’t want to extract paths manually, you can write a thin wrapper that reads the SVG markup at runtime.
import type { LucideProps } from 'lucide-react';
interface SvgIconProps extends LucideProps {
svgContent: string;
}
export function RawSvgIcon({ svgContent, size = 24, color, strokeWidth = 2, className }: SvgIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color ?? 'currentColor'}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
dangerouslySetInnerHTML={{ __html: svgContent }}
/>
);
}
Usage:
const myIconPaths = '<path d="M12 2 L15 8 L22 9 L17 14 L18 21 L12 18 L6 21 L7 14 L2 9 L9 8 Z"/>';
<RawSvgIcon svgContent={myIconPaths} size={20} className="text-primary" />
This approach trades the clean createLucideIcon API for flexibility with raw SVG content, useful when icons come from an API or CMS.
Only use dangerouslySetInnerHTML with SVG content you control. Never inject untrusted user-provided SVG markup; it can execute scripts in some browser contexts.
Matching the Lucide visual style in Figma
If you’re designing custom icons from scratch, set up Figma with these exact constraints to match Lucide:
- Frame size: 24x24px
- Stroke weight: 2px
- Stroke cap: Round
- Stroke join: Round
- Corner radius on paths: 0 (handle rounding through the stroke cap instead)
- Align to pixel grid: On
- Export: SVG, no “include id attribute”, no “outline stroke”
The most common mistake is exporting with “outline stroke” turned on in Figma. This converts your 2px stroke into a filled shape, and the resulting SVG path data is much more complex and will look slightly different at small sizes.
Icon accessibility for custom icons
Lucide sets aria-hidden="true" on all icons by default, assuming they’re decorative. For custom icons used as standalone interactive elements (inside a button with no visible label, for example), you need to add an accessible label.
<button>
<SparklesStack aria-hidden="true" size={20} />
<span className="sr-only">Generate sparkles</span>
</button>
For a standalone icon that conveys meaning without surrounding text:
<SparklesStack
role="img"
aria-label="Sparkles"
size={20}
/>
If removing the icon would make the UI incomprehensible, it needs an accessible label. If the surrounding text already communicates the meaning, aria-hidden is correct.
Testing your custom icons
Verify three things after adding any custom icon:
1. Visual match at all sizes. Render your icon alongside a built-in Lucide icon at 16px, 20px, 24px, and 32px. The stroke weight should look identical.
2. Dark mode. In dark mode, the icon should invert correctly. If it doesn’t, you have a hardcoded color somewhere in your SVG paths.
3. strokeWidth prop. Change strokeWidth from 1.5 to 2.5 and confirm the icon scales visually. If it stays the same, the stroke is baked into the path shape rather than the SVG stroke attribute.
// Quick visual test component
function IconTest() {
return (
<div className="flex gap-4 items-center">
{[16, 20, 24, 32].map(size => (
<div key={size} className="flex gap-2 items-center">
<Search size={size} />
<SparklesStack size={size} />
</div>
))}
</div>
);
}
Frequently asked questions
Can I use createLucideIcon with non-path elements like <circle> and <rect>?
Yes. The tuple format supports any SVG element. A circle icon:
const CircleDot = createLucideIcon('CircleDot', [
['circle', { cx: '12', cy: '12', r: '10', key: 'outer' }],
['circle', { cx: '12', cy: '12', r: '3', key: 'inner' }],
]);
Does createLucideIcon work with Lucide for Vue or Svelte?
The factory function is only part of lucide-react. Vue and Svelte users should use the defineComponent approach from lucide-vue-next or the component slot pattern in lucide-svelte. Both frameworks have similar extension patterns documented in their respective packages.
My custom icon looks thicker than Lucide icons at small sizes.
This usually means your SVG was exported at a larger viewBox (like 32x32 or 100x100) and not redrawn at 24x24. The stroke width scales with the viewBox, so a 2px stroke on a 32x32 grid looks heavier than a 2px stroke on 24x24 when rendered at the same size.