If you want off Lucide in a shadcn/ui project, pick a set drawn on the same 24×24 grid with a 2px stroke. Tabler Icons and Keyline Icons both are, so your components keep their spacing and the change is mostly an import path. Pick Phosphor for several weights of one drawing, and Heroicons for the Tailwind look, if a smaller set will do.
Keyline is this site's own set, so its weak spots are listed here like everyone else's.
Four sets that fit
| Set | Grid and stroke | Styles | Icons | React package |
|---|---|---|---|---|
| 24×24, 2px | Outline | 2,015 | lucide-react | |
| 24×24, 2px | Outline, and filled for 1,054 | 6,257 | @tabler/icons-react | |
| Its own grid | Six weights, from thin to duotone | 1,200+ | @phosphor-icons/react | |
| 24×24 at 1.5px, plus 20 and 16 solid | Outline, solid | 316 | @heroicons/react | |
| 24×24, 2px | Stroke, two-tone, duotone and fill, each rounded and sharp | 1,654 | @keyline-icons/react |
Counts are from each project's site or README in October 2026. All five are free for commercial use: Lucide under ISC, the rest under MIT.
bin, and Phosphor and Heroicons call search magnifying-glass.- Tabler matches the geometry and is by far the biggest. Only 1,054 of its icons have a filled version, so check yours before counting on filled active states. The stroke prop is
stroke, notstrokeWidth, and names are prefixed:IconBell. - Phosphor is the one for weights, picked with a
weightprop. Install@phosphor-icons/react, not the oldphosphor-react, and import from@phosphor-icons/react/ssrin Server Components. It draws rounder than the rest. - Heroicons draws its 16 and 20 pixel solid sets at those sizes rather than scaling them down, which is its best feature. The set is small and closed to new icon requests.
- Keyline has every icon in four styles, each with rounded or sharp corners: eight versions of every name, none missing. It's young, out of beta since September 2026, and a quarter the size of Tabler, so check coverage first if your product spans niche areas.
People usually leave Lucide for one of three reasons: no filled style to mark a selected item, a product that looks like every other shadcn app, or an icon it doesn't have. Borrowing that one icon from another set is how two stroke weights end up on one screen.
If none of the three is yours, stay. Lucide is good.
The swap is an import path
shadcn copies its components into your repo, so the imports to change live in your own files, components/ui/ included.
grep -rn --exclude-dir=node_modules 'from "lucide-react"' .Every set changes the import path. Three of them also change the names, and that is most of the work:
import { Search, Settings } from "lucide-react"
import { IconSearch, IconSettings } from "@tabler/icons-react"
import { MagnifyingGlassIcon, GearIcon } from "@phosphor-icons/react"
import { MagnifyingGlassIcon, Cog6ToothIcon } from "@heroicons/react/24/outline"
import { Search, Settings } from "@keyline-icons/react"Keyline keeps Lucide's names. Every icon shadcn's components import is there under the same name, CheckIcon as well as Check, except the spinner: Loader2Icon is LoaderCircleIcon. In your own code, the rename you'll meet most is the bin: Trash and Trash2 are Bin and Bin2. There's no absoluteStrokeWidth; the stroke scales with the icon.
To own the files instead of adding a dependency, the shadcn CLI copies single icons in as components that import nothing: npx shadcn add @keyline/bell.
shadcn sizes your icons, not you
Icon size is what catches people after a swap, whichever set they pick. shadcn's components size the SVGs inside them with Tailwind classes, and Button and Sidebar use different rules.
Button uses [&_svg:not([class*='size-'])]:size-4. Every SVG inside is 16px unless it carries a size-* class of its own:
<Button>
<Plus /> {/* 16px: the button sized it */}
<Plus size={32} /> {/* still 16px: the class beats the attribute */}
<Plus className="size-6" /> {/* 24px: the button steps aside */}
</Button>size={32} writes loses to the class, and a size-6 class opts out.Sidebar menu buttons use [&>svg]:size-4 instead, with no way out for your own class. To change icon size there, edit the class in components/ui/sidebar.tsx. If an icon won't resize after a swap, it's one of these two rules, not the new set.
Both rules land on 16px, so judge a set there, not at the 24 it was drawn on.
A selected item needs a second drawing
A selected nav item reads best as the same icon in a heavier style. That only works when the heavy version is the same drawing, and Lucide has no filled style, so its selected item can only change the background. In Keyline each style is its own import:
import { House } from "@keyline-icons/react"
import { House as HouseFill } from "@keyline-icons/react/fill"
function NavIcon({ active }: { active: boolean }) {
const Icon = active ? HouseFill : House
return <Icon />
}lucide-react and on @keyline-icons/react. Only the second can switch Home to its fill drawing. Lucide icons © Lucide Icons and Contributors, ISC licence.Phosphor does the same with one prop, weight={active ? "fill" : "regular"}, Heroicons with an import from @heroicons/react/24/solid, and Tabler with IconBell and IconBellFilled, where a filled version exists.
To choose between two sets, swap the sidebar first. It has both sizing rules and a selected item, so it shows every problem here before the rest of the app moves.
Getting it
Switch from Lucide Every name and prop that differs, and the shadcn registry setup.
Browse the icons Every drawing, all four styles, both corner shapes.
Custom icons A family no set covers, drawn on the same grid as the free ones.