Frameworks
How to Add SVG Icons to shadcn/ui: The Complete Guide
Add SVG icons to a shadcn/ui project the right way: lucide-react defaults, swapping in Heroicons, Tabler or Phosphor, custom icons, sizing and accessibility rules.
shadcn/ui icons come from Lucide by default, and for most projects that default is correct. This guide covers when to keep it, how to swap in a different set cleanly, and how to add your own custom SVG icons without fighting the framework.
Why shadcn/ui treats icons as code, not assets
shadcn/ui is not a component library you install and forget. The CLI copies component source into your repository, usually under components/ui, and from that moment the code is yours to edit. Icons follow the same philosophy. A copied component such as the Select or the Dialog arrives with its icon imports written directly into the file, pointing at lucide-react. There is no theme object that resolves icon names at runtime and no font file to load. The icon you see in the rendered page is exactly the component imported at the top of the file, which means every question about shadcn/ui icons is really a question about imports, props and class names.
This design has two practical consequences. First, icon choices are per component and fully visible in code review, so drift is easy to spot. Second, there is no central switch that re-skins every icon at once. If you want a different family, you change imports file by file, or you build a thin wrapper layer and point components at it. Both approaches are covered below, and both are cheap if you do them before the component count grows.
The default: lucide-react
Every shadcn/ui component that needs an icon imports it from lucide-react: the ChevronDown in a Select, the X in a Dialog, the Check in a Checkbox. The set is consistent (2px stroke, 24px grid) and the licence (ISC) is safe for commercial work. Install it once and import per icon so tree-shaking keeps the bundle small.
npm install lucide-reactimport { Search, Settings, User } from "lucide-react";
<Button>
<Search className="mr-2 h-4 w-4" /> Search
</Button>What you get out of the box
A standard shadcn/ui install uses a surprisingly small slice of Lucide. The Accordion and Collapsible use ChevronDown, the Select adds Check and ChevronUp, the Dialog and Sheet use X, the Checkbox and Radio Group rely on Check and a filled Circle, the Calendar leans on ChevronLeft and ChevronRight, and the Toast variants pull in status glyphs. Because each copied file names its own icons, you can grep the project for lucide-react and see the complete inventory in seconds. That inventory is the real design system: a few dozen glyphs, one stroke weight, one sizing scale. Protecting that coherence matters more than any single icon choice.
Setting project-wide defaults with a wrapper
Lucide icons accept size, strokeWidth and className props, and repeating the same values across hundreds of call sites invites drift. The fix the community converged on is a small wrapper that fixes your defaults and forwards everything else. Create components/icon.tsx, wrap lucide-react there, and import from the wrapper in your own feature code. Copied shadcn/ui components can keep importing lucide-react directly, since their defaults already match the house style of 16px icons inside controls.
import type { LucideProps } from "lucide-react";
import { Search as LucideSearch } from "lucide-react";
export function SearchIcon(props: LucideProps) {
return <LucideSearch size={18} strokeWidth={2} aria-hidden {...props} />;
}The wrapper is also the right home for accessibility defaults. Baking aria-hidden into decorative wrappers means feature code cannot forget it, and the one place an icon is meaningful on its own can pass an explicit override together with a proper label on its button.
Swapping in a different icon set
If your product already uses another family, replace the imports component by component rather than installing a second system globally. Heroicons and Tabler both match Lucide's outline weight closely; Phosphor works too if you standardise on its Regular weight. Preview candidates side by side in the Lucide, Tabler and Heroicons sets before committing. The rule that keeps shadcn/ui icons coherent: one set per app, no exceptions for a single cute icon.
A safe swap workflow
Swapping families touches many files, so treat it like a migration, not a find-and-replace party. Work in this order and the change stays reviewable:
- Inventory first: search the codebase for lucide-react and list every icon name in use. Most projects use fewer than sixty.
- Build a mapping table from old names to new names. A few will not have equivalents, and those decide feasibility, so find them early.
- Install the new package, then migrate one component group at a time: primitives first, then feature screens, checking visuals after each group.
- Normalise sizes as you go. Heroicons outline icons are drawn for 24px with a 1.5 stroke, so a control tuned for Lucide at 16px usually wants h-5 w-5 in Heroicons to read at the same weight.
- Delete lucide-react only after the final grep returns nothing, and keep the mapping table in the pull request description for reviewers.
The three realistic target packages each have a personality. @heroicons/react ships outline and solid variants plus a 20px mini set, which maps neatly onto default and hover states. @tabler/icons-react offers the broadest catalogue of the three and matches Lucide's 2px stroke, making it the least visually disruptive swap. @phosphor-icons/react gives six weights per glyph, which is powerful for marketing pages but risky in dense product UI, where a stray Bold weight reads as a different icon system. Whichever you pick, the libraries directory lists every set with licence and counts so you can sanity-check coverage before committing.
// Before
import { ChevronDown } from "lucide-react";
// After (Heroicons)
import { ChevronDownIcon } from "@heroicons/react/24/outline";Adding a custom SVG icon
For a logo or a glyph no library has, create a small component that accepts className, uses currentColor, and sets aria-hidden when decorative. Keep the original viewBox (usually 0 0 24 24) and remove hard-coded fills so the icon inherits your theme. IconVault's SVG to JSX converter produces exactly this component shape from any SVG file.
A production-ready custom icon component looks like this. Note the spread of props at the end, which lets callers override size and class, and the stroke settings that match Lucide so the custom glyph can sit next to library icons without looking foreign.
import type { SVGProps } from "react";
export function RocketIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
<path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
</svg>
);
}Before pasting any found SVG into a component, run it through the SVG optimizer. Designer exports arrive with editor metadata, unused groups, decimal-heavy paths and sometimes fixed width and height attributes that fight your className sizing. Optimising first, then converting, keeps the component diff small and the rendered result predictable. Store finished customs in components/icons, one file per icon, exported from a single index so imports read like library imports.
Brand marks are the one exception
The one-set rule governs interface glyphs, not logos. Payment marks, social logos and your own product mark live outside the icon system and should use their official artwork, usually from Simple Icons, with original colours intact. Keep them in the same components/icons folder but name them clearly, LogoGitHub rather than GithubIcon, so nobody restyles them into the theme by accident.
Sizing and accessibility rules
- 16px (h-4 w-4) inside buttons and menu items; 20px for standalone toolbar actions.
- Icons that carry meaning alone need an accessible label: a visible text label, sr-only text, or aria-label on the button.
- Decorative icons get aria-hidden="true" so screen readers skip them.
- Never recolor a status icon away from its semantic color: green check, red error, amber warning.
Stroke width is part of the system
Two icons at the same pixel size can look mismatched purely because of stroke. Lucide draws at a 2px stroke on a 24px grid, and at 16px that stroke scales down proportionally, which is why small Lucide icons stay legible. If you mix in a set drawn at 1.5px, its icons look lighter and slightly larger optically. Compensate deliberately: either render the lighter set one step smaller, or raise its strokeWidth prop where the component allows it. Optical alignment beats mathematical alignment every time, and your eyes are the test tool. Preview mixed candidates at production size in the browse grid before merging anything.
Icons in dark mode and themes
shadcn/ui themes with CSS variables, and icons inherit the current text colour through currentColor, so dark mode normally needs no icon-specific work at all. Problems appear when a copied component or a custom SVG hard-codes a fill such as fill="#000". Hunt for those during the swap audit. Semantic colours are the exception that proves the rule: a destructive button's trash icon should follow the destructive token, not the foreground token, so it reads as part of the action. With Tailwind, prefer text-destructive on the icon or inherit it from the button variant rather than bolting on arbitrary hex values.
Icons in menus, dialogs, toasts and forms
Most shadcn/ui icon work happens inside composite components, and each has conventions worth copying. Dropdown menus and context menus place a 16px icon before the label with a consistent mr-2, and destructive items colour icon and text together. Command palette items in cmdk follow the same pattern, which is why the Command component looks native the moment you match its spacing. Dialogs and sheets keep their close X icon-only, so the button needs an sr-only Close label, a detail many templates skip and every accessibility audit flags.
Forms are subtler. An icon inside an Input, such as a Search glyph in a filter field, is decorative even though it hints at purpose, because the input itself carries the label. Status icons in form messages are the opposite: the Check or CircleAlert beside an error or success message reinforces meaning that colour alone should never carry. Toasts from Sonner accept icon overrides per toast type; use them sparingly and keep the default status glyphs, since users learn toast iconography fast and custom glyphs slow recognition. When in doubt, mirror what the copied shadcn/ui component already does, because that consistency is what makes the system feel like one product.
Tree-shaking and bundle discipline
Icon packages are the classic silent bundle leak. The rules for shadcn/ui icons are simple and absolute:
- Import icons by name from the package root, as in import { Search } from "lucide-react". The package is built for per-icon tree-shaking, so this is both the documented and the cheapest pattern.
- Never import a namespace or a full icon font package to grab two glyphs. One careless barrel import can pull hundreds of unwanted components into the client bundle.
- Do not build runtime icon pickers from the full library unless the page genuinely is an icon browser. If you need dynamic names, map a small allow-list of components yourself.
- Check the result with your bundler's analyser after any icon package change. Icon regressions are invisible in code review and obvious in a bundle report.
Server components deserve one extra note. lucide-react and its peers are plain presentational components with no hooks or browser APIs, so they render inside React Server Components without a client boundary. The bundle cost lands only where a client component imports them. Keeping chrome such as headers and sidebars as server components, with icons imported there, is free; the same icons inside a client-heavy dashboard are not. Our Next.js icon guide walks through that boundary in detail.
Common errors and fixes
Module not found: lucide-react after copying a component
The add command copies component source that assumes lucide-react exists. On older templates or minimal starters it may not be installed yet. The fix is the install command from the top of this guide, not editing the copied file. If several copied components fail at once, it is always the missing package, never the components.
The custom icon renders solid black
Almost always a fill problem. The pasted SVG carries fill attributes on its paths, or the root svg sets fill without currentColor, so theme colour never applies. Remove per-path fills, set fill="currentColor" for filled glyphs or fill="none" with stroke="currentColor" for outline glyphs, and confirm no presentation attribute is overriding your CSS. Converting through the SVG to JSX tool and then cleaning fills takes under a minute.
className has no effect on a custom icon
If width and height attributes survived on the root svg element, they win over some sizing expectations and make the component look unresponsive to h-4 w-4. Delete the fixed attributes, keep the viewBox, and make sure the component spreads props onto the svg element. A custom icon that does not spread props cannot receive className at all, which is the other half of this bug.
Icons look heavier or lighter than the rest of the UI
That is a stroke or grid mismatch, usually a custom glyph drawn on a 32px grid sitting among 24px-grid icons. Rescale the artwork into a 24 viewBox, match the 2px stroke convention, and compare at 16px, the size where mismatches shout. If the glyph came from a filled set, no adjustment will make it blend; choose an outline equivalent instead of shipping a visual accent you did not intend.
Migrating from react-icons or Font Awesome
Many shadcn/ui adopters arrive with react-icons or Font Awesome already in the tree. Migrating is worth it for coherence: react-icons aggregates dozens of sets with different grids and stroke logic, so screens assembled from it rarely look like one system. Migrate screen by screen using the same mapping-table workflow as a family swap, and resist keeping react-icons 'just for brand logos'; Simple Icons as components covers that need without the grab-bag. Font Awesome migrations carry one extra decision, since its free solid style has no outline equivalent for many glyphs. Map to the closest Lucide outline icon and accept small visual changes, or keep Font Awesome only on legacy pages behind a clear boundary. Either way, finish the migration; two half-used systems is the worst steady state.
Done well, shadcn/ui icons disappear into the interface, which is the point. For the wider ecosystem view, read our comparison of the best React icon libraries.
Icons inside Radix primitives and overlays
shadcn/ui is built on Radix primitives, and overlays introduce two icon situations worth knowing. First, content rendered in portals, such as dropdowns, popovers and tooltips, mounts outside your component tree in the DOM, so global CSS you scoped to a page section will not reach it; icon styling must travel with the component classes, which is another reason the copied components style icons inline rather than through page stylesheets. Second, Radix manages focus inside dialogs and menus, and decorative icons inside focusable items should stay aria-hidden so the item's label remains the single accessible name. If you customise the close button of a Dialog or the indicator of a Checkbox, keep the primitive's structure and swap only the glyph; re-implementing the anatomy to fit an icon is how keyboard behaviour quietly breaks.
Dynamic icon names and icon pickers
Settings screens and admin tools sometimes need an icon chosen at runtime: a sidebar link editor, a category manager, a workspace switcher. The wrong pattern is importing the entire Lucide namespace and indexing it by string, which defeats tree-shaking and ships the full set for one feature. The right pattern is an explicit allow-list that maps the names you support to their components. The list stays small because it is curated by you, it type-checks, and the picker UI can render options straight from the same map. When a user-facing name differs from the component name, store the user's key and translate in one place rather than scattering component imports across the feature.
import { Home, Search, Settings, User } from "lucide-react";
export const PICKABLE_ICONS = {
home: Home,
search: Search,
settings: Settings,
user: User,
} as const;
export type IconName = keyof typeof PICKABLE_ICONS;Keeping icons current when components update
Because shadcn/ui components are copied source, re-adding a component with the CLI can overwrite your icon swaps inside it. Treat icon customisation as a layer you can reapply: keep your mapping table in version control, note in the project readme which primitives were re-iconed, and after any component update, diff the file and reapply the import changes deliberately. Pin lucide-react and any replacement package to tested versions and upgrade them in dedicated pull requests, since icon sets occasionally rename glyphs between releases. A rename is a five-minute fix when it arrives alone and a confusing scavenger hunt when it rides along with a feature release, which is why icon upgrades deserve their own boring, reviewable commits.
The shadcn/ui icon checklist
- One icon family for interface glyphs, documented in the readme, with brand logos as the only exception.
- Icons imported by name, never through a namespace or a full-set registry, outside an explicit picker allow-list.
- Default size and stroke fixed in a wrapper or matched to the copied components' built-in conventions.
- Decorative icons aria-hidden by default; icon-only buttons carry sr-only text or an aria-label.
- Custom glyphs optimised, converted to components with currentColor, and stored in components/icons.
- Bundle checked after any icon package change, and component re-adds followed by a re-application of known icon swaps.
Choosing a set before the first component lands
Decide the family before you copy components. The cheapest moment to choose shadcn icons is before the first Button lands in the codebase, because every copied primitive after that point hard-codes the decision in its imports. When teams evaluate the best shadcn ui icons for Next.js, they often start from screenshots, but the durable test is a coverage check: list the fifty glyphs your product will actually use and confirm the set draws all of them well. A set that covers navigation, forms, feedback and commerce without gaps will serve shadcn icons in production far better than a prettier set that forces one-off substitutions. The best shadcn ui icons for Next.js are therefore the ones that survive your real list, not the ones that win a beauty contest on a landing page.
Judge stroke, grid and licence together. A coherent system for shadcn icons shares one stroke weight and one grid, so compare candidates at 16px, the size where most interface shadcn icons actually render, rather than at poster size. Lucide draws at a 2px stroke on a 24px grid, Tabler matches that weight with wider coverage, and Heroicons draws lighter at 1.5px, which changes how dense toolbars feel. Licence matters just as much: the best shadcn ui icons for Next.js carry permissive terms such as ISC or MIT, so commercial shipping needs no legal review, while a CC BY set adds an attribution chore to every deployment. Pick the family that passes all three tests at once, and shadcn icons stop being a recurring debate and become a settled default.
Icon tokens with Tailwind and CSS variables
Turn sizes into tokens, not scattered classes. As shadcn icons spread across an app, ad-hoc h-4 w-4 and h-5 w-5 classes drift until the same action looks different on two screens. A token layer fixes this: define icon sizes such as sm, md and lg once, map them to Tailwind utilities in your wrapper, and let every feature import the token instead of a raw class. Teams choosing the best shadcn ui icons for Next.js benefit most from this discipline, because Next.js apps tend to grow both a marketing site and a dashboard in one repository, and tokens are what keep shadcn icons consistent across both surfaces. The best shadcn ui icons for Next.js feel invisible precisely when a token system removes the need to think about each instance.
Let colour tokens do the theming work. Because shadcn icons inherit currentColor, a token such as text-muted-foreground or text-destructive colours every glyph without icon-specific CSS, and dark mode follows automatically when the theme variables flip. The failure mode to avoid is a hard-coded hex inside a custom SVG, which ignores tokens and glows wrong in dark mode; the audit for this is a simple grep for fill and stroke hex values across components/icons. When shadcn icons are token-driven, a rebrand becomes a variable change rather than a file-by-file hunt, and that is the real payoff of treating shadcn icons as a system instead of a pile of imports.
Dense interfaces: tables, sidebars and toolbars
Alignment is the whole job in dense UI. In a data table or a sidebar, shadcn icons sit next to small text at 13 or 14px, and a single misaligned glyph makes the row feel broken. The reliable pattern is a fixed 16px box for every row icon, vertical centring through flex alignment, and identical left padding across rows, so the eye reads a straight column of glyphs rather than a jitter. When developers compare the best shadcn ui icons for Next.js dashboards, they should preview candidates inside a real table row, not on a showcase page, because optical weight at small sizes decides whether shadcn icons read as calm or noisy. The best shadcn ui icons for Next.js dashboards are the ones that stay quiet at 16px and never compete with the data beside them.
Reserve space for icons that appear conditionally. Sorting indicators, row actions and status glyphs that pop in on hover cause layout shift when their slot has no reserved width, and users perceive that shift as instability even when the data is correct. Give sortable headers a permanent icon slot, render action buttons with opacity transitions instead of mounting and unmounting them, and keep status icons in a fixed-width cell. These habits cost nothing at build time and protect the polish that shadcn icons are supposed to deliver. In toolbars, the same rule applies to icon-only buttons: a consistent 32 or 36px hit area, identical across the bar, makes shadcn icons feel like a designed control set rather than a loose collection of glyphs.
Marketing pages inside the same app
Display icons play by different rules. A Next.js repository often serves a marketing site and an application from the same codebase, and shadcn icons on a landing page are display graphics at 32 or 48px, not controls at 16px. At display size, a 2px stroke looks thin, so marketing sections usually want a heavier stroke or a filled variant for the same glyph, and that is acceptable as long as the change is deliberate and documented. Teams picking the best shadcn ui icons for Next.js should therefore test their shortlist at both sizes: the set that wins at 16px may look anemic at 48px, and shadcn icons need a documented display treatment rather than an accidental one. The best shadcn ui icons for Next.js handle both scales when the team defines a display style up front instead of improvising per section.
Keep marketing and product icons recognisably related. The safest bridge between a landing page and an app is to keep the same family and vary only weight or size, so a visitor who signs up meets glyphs they already recognise from the homepage. If marketing demands a second, more expressive family for illustrations and feature graphics, confine it to hero and feature sections and never let it leak into the application chrome. Document the boundary in the same place you document shadcn icons for product UI, because the next designer will otherwise blend the two systems gradually. Consistency across the funnel is a conversion asset: users trust a product whose shadcn icons look like the marketing that sold it to them.
Motion: spinners, transitions and micro-interactions
Animate state, not decoration. The most useful motion for shadcn icons is state feedback: a spinner that replaces a button label while a form submits, a check that draws in when an action succeeds, a chevron that rotates when an accordion opens. These animations are small, fast and meaningful, and they work because the icon itself communicates the state change rather than decorating it. Developers evaluating the best shadcn ui icons for Next.js should check that their set's spinner glyph is a clean circular arc, since a decorative loader built from a generic icon looks cheap; shadcn icons in loading states deserve a purpose-drawn glyph. The best shadcn ui icons for Next.js pair with Tailwind's animate-spin and transition-transform utilities without custom CSS, which keeps motion consistent with the rest of the system.
Respect reduced motion everywhere icons move. Some users experience motion as discomfort, and the correct response is to honour prefers-reduced-motion for every icon animation, not just the large ones. Tailwind's motion-safe and motion-reduce variants make this a one-line habit: animate-spin becomes motion-safe:animate-spin, and a rotating chevron gets motion-reduce:rotate-0 so it simply appears in its final state. Audit shadcn icons in loading, accordion and drawer components for this treatment, because those are the places motion concentrates. A system that moves thoughtfully and stops when asked is what separates polished shadcn icons from merely animated ones.
Auditing icons every quarter
Schedule an icon audit like a dependency audit. Icon sets drift silently: a new screen imports a second family for one glyph, a designer exports a custom mark that duplicates a library icon, a deprecated glyph lingers after a refactor. A quarterly audit catches all three before they compound, and it takes under an hour: grep the codebase for icon imports, list every family in use, and compare the list against the documented system. When teams maintain the best shadcn ui icons for Next.js over a year, this audit is the habit that preserves the original decision, because shadcn icons only stay coherent while someone checks. The best shadcn ui icons for Next.js are rarely the newest set; they are the set a team actually kept consistent through four quarters of feature work.
Measure the audit in bundle bytes, not opinions. After listing imports, run the bundle analyser and record the icon packages' contribution, then remove unused glyphs, delete the second family if one crept in, and replace custom duplicates with library equivalents where the meaning matches. Report the before-and-after numbers in the pull request, because a 40KB saving is a more persuasive argument for discipline than any style debate. Keep the audit output in the repository, next to the mapping table from the original migration, so the next audit starts from evidence instead of memory. Over time, this ritual turns shadcn icons from a one-time setup into a maintained asset, which is exactly how mature design systems treat every other shared dependency.
Designer handoff and Figma parity
Name parity between Figma and code prevents silent drift. The most common designer-developer icon bug is not a missing glyph but a renamed one: Figma calls it arrow-right while the code imports ArrowRight, and a search for either name misses the other half of the system. Agree on one naming convention, usually the library's component name, and use it in Figma layer names, in the icon inventory document, and in code. When the team reviews the best shadcn ui icons for Next.js, they should confirm the set's Figma library exists and matches the package version, because shadcn icons drawn from a stale Figma file arrive in code already wrong. The best shadcn ui icons for Next.js are the ones whose design file and package stay in lockstep through upgrades.
Treat exports as a pipeline, not a handoff event. Designer-exported SVGs arrive with editor metadata, artboard-sized viewBoxes and hard-coded fills, all of which fight the component conventions this guide sets out, so every export should pass through the optimiser and the JSX converter before it reaches components/icons. Record the source file and the export settings alongside the component, so a future redraw can be re-exported identically instead of re-cleaned by hand. Review custom glyphs at 16px in the actual product UI, not at 200% in Figma, because stroke and spacing decisions that look generous on a canvas look cramped in a toolbar. A disciplined export pipeline is what lets shadcn icons from designers sit invisibly next to library glyphs.
Multi-brand and white-label theming
Tokens make white-label icons possible. When one codebase serves several brands, shadcn icons must adapt per tenant without per-tenant code branches, and the token layer is what makes that feasible. Brand colour arrives as CSS variables per tenant, icons inherit currentColor, and the only per-brand assets are logos, which live outside the icon system by design. Teams building the best shadcn ui icons for Next.js white-label products should verify early that no icon hard-codes a brand colour, because a single hex value in a shared component breaks every tenant theme at once. With shadcn icons fully token-driven, onboarding a new brand becomes a theme file, not a code change, and that is the difference between a white-label product and a fork.
Guard the exceptions explicitly. White-label apps still need a few brand-specific glyphs, such as a tenant's product mark or a partner logo, and those belong in a clearly separated folder with a naming rule that marks them as tenant assets, never in the shared icon directory. A component that renders a tenant mark should accept the tenant as a prop and fall back to a neutral glyph when no mark exists, so a missing asset degrades to a clean default instead of an empty box. Document which shadcn icons are shared and which are tenant-scoped, because the next engineer will otherwise copy a tenant glyph into shared UI and ship one customer's brand to everyone. Boundaries written down are boundaries kept.
Documenting the system for the next developer
Write the icon readme before you need it. Every icon system eventually meets a developer who was not present at its creation, and that developer's first question is always which set, which sizes and where custom glyphs live. A short readme answering those three questions, with the mapping table and the wrapper location, saves a week of archaeology over the life of a project. Teams that maintain the best shadcn ui icons for Next.js treat this document as part of the system, updated in the same pull request as any icon change, so shadcn icons never become tribal knowledge. The best shadcn ui icons for Next.js are documented ones, because an undocumented system is one resignation away from becoming two systems.
Show, do not just tell, in Storybook. A Storybook page that renders every approved icon at its approved sizes, with its name and import path beside it, turns the readme into a tool developers actually open. Add a story for the wrapper defaults, one for icon-only buttons with their accessible labels, and one for the dark theme, so visual regressions surface in the same review loop as component changes. When a new glyph is proposed, its story entry forces the conversation this guide wants: does it belong to the family, does it duplicate an existing glyph, and does it pass the 16px test. That review habit is how shadcn icons stay a system as the team grows beyond its founders.
Icon states: hover, active, disabled and loading
Give every interactive icon four visible states. For shadcn icons, consistency across hover, active, disabled and loading states matters more than any single glyph choice, and shadcn icons that share one treatment for these states feel like a designed system rather than a loose collection. Teams choosing the best shadcn ui icons for Next.js should verify that their set draws a clean spinner, because shadcn icons in loading states are the ones users stare at longest. The best shadcn ui icons for Next.js also keep disabled glyphs legible at reduced opacity, so shadcn icons never disappear entirely when an action is unavailable.
Disabled icons must stay recognisable, not vanish. For shadcn icons, the standard treatment is the same glyph at reduced opacity with the pointer events removed, and shadcn icons handled this way still communicate what the action would be. Teams evaluating the best shadcn ui icons for Next.js should test disabled states in both themes, because shadcn icons that pass in light mode often fail contrast in dark mode. The best shadcn ui icons for Next.js pair this with a consistent hover treatment, so shadcn icons always signal interactivity the same way across the whole application.
Responsive behaviour for icons on small screens
Shrink the chrome, not the meaning. For shadcn icons, small screens force a choice between keeping every toolbar glyph and keeping the layout usable, and shadcn icons in a collapsing toolbar should hide low-priority actions behind a menu rather than shrinking below 16px. Teams building the best shadcn ui icons for Next.js mobile layouts should define which icons survive at each breakpoint, because shadcn icons that crowd a 360px viewport make the product feel broken. The best shadcn ui icons for Next.js handle this through a documented responsive plan, so shadcn icons degrade gracefully instead of overflowing.
Touch targets matter more than pixel sizes. For shadcn icons, a 16px glyph inside a 44px touch target is correct on mobile, and shadcn icons that sit in smaller targets cause mis-taps that users blame on the product. Teams designing the best shadcn ui icons for Next.js responsive screens should audit every icon-only button's hit area at the smallest breakpoint, because shadcn icons in cramped headers are the usual offenders. The best shadcn ui icons for Next.js keep the same glyph across breakpoints and change only the surrounding chrome, so shadcn icons stay familiar while the layout adapts around them.
Directional icons and right-to-left layouts
Arrows must flip when the layout flips. For shadcn icons, directional glyphs such as back, forward and chevrons need mirroring in right-to-left layouts, and shadcn icons that ignore direction make navigation feel wrong for a large audience. Teams shipping the best shadcn ui icons for Next.js internationally should use logical utilities that flip automatically, because shadcn icons handled with explicit per-language overrides rot quickly. The best shadcn ui icons for Next.js document which glyphs are directional and which are not, so shadcn icons like search or settings stay put while arrows mirror correctly.
Test direction with real content, not screenshots. For shadcn icons, the reliable check is running the application in an RTL locale and tabbing through navigation, dialogs and pagination, and shadcn icons that fail this test usually fail because a transform was applied to the wrong element. Teams maintaining the best shadcn ui icons for Next.js across locales should add one RTL screenshot to their visual regression set, because shadcn icons in mirrored layouts are easy to verify and embarrassing to miss. The best shadcn ui icons for Next.js treat direction as a first-class requirement, so shadcn icons never become an afterthought for international users.
A process for requesting new icons
New glyphs should arrive through a request, not a shortcut. For shadcn icons, the healthiest process is a short request template asking for the meaning, the screen and the closest existing glyph, and shadcn icons added through this process rarely duplicate something the set already covers. Teams running the best shadcn ui icons for Next.js at scale should review requests weekly, because shadcn icons requested ad hoc during feature work are how second families sneak into a codebase. The best shadcn ui icons for Next.js stay coherent when every addition is judged against the existing inventory, so shadcn icons grow deliberately instead of accidentally.
Reject requests that the set already answers. For shadcn icons, most requests turn out to be a naming problem rather than a coverage gap, and shadcn icons already in the set can usually serve once the requester sees them at production size. Teams stewarding the best shadcn ui icons for Next.js should answer requests with a screenshot of the closest existing glyph first, because shadcn icons multiply fastest when nobody checks. The best shadcn ui icons for Next.js grow slowly by design, so shadcn icons added this year still belong to the same visual family next year.
Onboarding new developers onto the icon system
A thirty-minute onboarding saves weeks of drift. For shadcn icons, new developers should be shown the wrapper, the inventory document and the mapping table on their first day, and shadcn icons explained this way become a system they respect rather than a folder they improvise in. Teams hiring around the best shadcn ui icons for Next.js should include the icon conventions in their onboarding checklist, because shadcn icons are usually the first thing a newcomer touches and the easiest place to introduce inconsistency. The best shadcn ui icons for Next.js are taught, not assumed, so shadcn icons stay consistent even as the team turns over.
Pair the first icon task with a review. For shadcn icons, the most effective onboarding step is a small first task that adds or swaps a glyph, reviewed against the checklist in this guide, and shadcn icons handled in a reviewed first task teach the conventions better than any document alone. Teams scaling the best shadcn ui icons for Next.js should treat that review as a teaching moment, because shadcn icons reviewed early prevent the habits that audits later punish. The best shadcn ui icons for Next.js survive team growth when every developer's first icon change is reviewed, so shadcn icons remain a shared standard rather than personal preference.
Frequently asked questions
Can I use icons other than Lucide in shadcn/ui?
Yes. shadcn/ui components are your own copied source, so any React icon component works anywhere a Lucide icon does. Swap imports component by component, normalise sizes for the new set's stroke and grid, and keep one family across the app. Heroicons, Tabler and Phosphor are the popular alternatives, and each has a dedicated React package.
Why does my custom SVG icon ignore Tailwind size classes?
Two causes cover nearly every case. The svg element still has fixed width and height attributes from the original export, or the component never spreads props onto the svg, so className is silently dropped. Remove the fixed attributes, keep the viewBox, spread props last, and the icon will obey h-4 w-4 like any library icon.
Do lucide-react icons work in React Server Components?
Yes. Lucide icons are stateless SVG components with no hooks, effects or browser APIs, so they render inside Server Components and stream with the page. The same applies to most component icon packages. You only cross a client boundary when the component that imports the icon is itself a client component.
How do I change the default icon size across a whole project?
Create a wrapper component that fixes size and strokeWidth and forwards all other props, then import icons through it in your feature code. Copied shadcn/ui primitives can keep their built-in sizing, which already matches control heights. A wrapper also centralises accessibility defaults such as aria-hidden for decorative use.
Is it a problem to use two icon sets in one shadcn/ui app?
For interface icons, yes. Mixed grids and stroke weights are visible at small sizes and make the UI feel assembled rather than designed. The accepted exception is brand artwork: official logos in their original colours are not part of the icon system and should stay accurate rather than being redrawn to match.
Find the icons from this guide
Search 421,020 free SVG icons from 239 libraries in one place.