Migrating to lucide icons

What changes when Apsara replaces @radix-ui/react-icons with lucide, and how to keep the old appearance.

Apsara used to draw its icons with @radix-ui/react-icons. It now draws them with lucide, behind stable keys you can replace one at a time.

This is a breaking release: some icons look different, lucide-react is a new peer dependency, and the names @raystack/apsara/icons exports have changed.

1. Install the peer dependency

1npm install lucide-react

The range is wide — >=0.500.0 <1.0.0 — so your app picks the version. If a lucide release changes a drawing you care about, replace that one icon (step 5) rather than pinning the whole library.

@radix-ui/react-icons is no longer a dependency of Apsara. If your own code imports from it, keep it in your own dependencies.

2. Rename what you imported from @raystack/apsara/icons

That path used to export raw in-house SVG components. It now exports the 31 icons Apsara's components draw, as replaceable icon components. Twelve of the old names are gone.

Removed nameUse insteadAppearance
BellIconlucide BellSame glyph
BellSlashIconlucide BellOffSimilar
BuildingsFilledIconlucide Building2Solid becomes stroke
CheckCircleFilledIconlucide CircleCheckSolid becomes stroke
CoinIconlucide CoinsSimilar
CoinColoredIconlucide CoinsLoses its colour
CrossCircleFilledIconlucide CircleXSolid becomes stroke
OrganizationIconlucide Building2Similar
ResetIconlucide RotateCcwSimilar
ShoppingBagFilledIconlucide ShoppingBagSolid becomes stroke
SidebarIconPanelLeftIcon, or lucide PanelLeftSimilar
TriangleRightIconChevronRightIconSolid triangle becomes a chevron

Two names survive, both with a new drawing:

  • CoPilotIcon — lucide Sparkles in place of the in-house solid sparkle.
  • FilterIcon — lucide ListFilter in place of the in-house solid funnel.

A raw lucide component draws 24×24 at strokeWidth={2}, so set size={16} strokeWidth={1.5} at the call site to match the Apsara icons beside it — or wrap it once with createIcon, which applies those for you:

1// src/icons.ts
2import { createIcon } from '@raystack/apsara/icons';
3import { Bell } from 'lucide-react';
4
5export const BellIcon = createIcon('BellIcon', Bell);

See Building your own icons.

3. Check the icons that changed shape

These are inside Apsara's own components, so they change without you touching a call site. Everything else is the same glyph in a different drawing style.

WhereBefore (radix)AfterWhat changed
Sidebar collapseViewVerticalIconPanelLeftIconA different glyph
Sidebar group toggleTriangleDownIconChevronDownIconA solid triangle becomes a chevron
Menu and ContextMenu submenu markerin-house TriangleRightIconChevronRightIconA solid triangle becomes a chevron
ChatPanel expandSizeIconExpandIconA different glyph
ChatPanel minimizeMinusIconShrinkIconA dash becomes the matched pair of ExpandIcon
PromptInput stopStopIconStopIcon (lucide Square)Solid becomes stroke
DataTable sort ascendingTextAlignTopIconSortAscendingIconA different glyph
DataTable and DataView sort descendingTextAlignBottomIconSortDescendingIconA different glyph
DataTable and DataView display settingsMixerHorizontalIconDisplayIconSimilar
DataTable and DataView filtersin-house FilterIconFilterIcon (lucide ListFilter)A solid funnel becomes filter lines
ChatPanel minimized bubblein-house CoPilotIconCoPilotIcon (lucide Sparkles)A solid sparkle pair becomes a stroked sparkle

Two more are worth a look, though the glyph is nearly the same:

  • DatePicker and RangePicker draw CalendarIcon, which is lucide CalendarDays, so the glyph has day marks inside it.
  • Search's clear button and Toast's error status draw CircleX in place of radix CrossCircledIcon.

4. Expect a 1px size change in some places

Every Apsara icon renders at 16×16 with strokeWidth={1.5}, which draws the 1px stroke of the design because lucide's viewBox is 24 units wide. The radix icons were intrinsically 15×15.

  • A call site that set no size grows from 15px to 16px.
  • A call site that set a CSS class is unaffected — CSS beats an SVG presentation attribute.
  • A call site that set width/height explicitly is unaffected — your props are applied after Apsara's base values.

To change the size or the stroke of every icon at once, use the props half of <Theme icons>:

1<Theme icons={{ props: { width: 20, height: 20, strokeWidth: 1.25 } }}>

5. If you want the radix appearance back

Apsara ships no radix preset, so register the radix icons yourself at <Theme>. Keep @radix-ui/react-icons in your own dependencies and copy this map:

1'use client';
2
3import {
4 ArrowDownIcon,
5 ArrowUpIcon,
6 CalendarIcon,
7 CheckCircledIcon,
8 CheckIcon,
9 ChevronDownIcon,
10 ChevronLeftIcon,
11 ChevronRightIcon,
12 CopyIcon,
13 Cross1Icon,
14 CrossCircledIcon,
15 DotsHorizontalIcon,
16 ExclamationTriangleIcon,
17 FileTextIcon,
18 InfoCircledIcon,
19 MagnifyingGlassIcon,
20 MinusIcon,
21 MixerHorizontalIcon,
22 MoonIcon,
23 PlusIcon,
24 SizeIcon,
25 StopIcon,
26 SunIcon,
27 TableIcon,
28 TextAlignBottomIcon,
29 TextAlignTopIcon
30} from '@radix-ui/react-icons';
31import { Theme, type IconOverrides } from '@raystack/apsara';
32
33const radixIcons: IconOverrides = {
34 ArrowDownIcon: ArrowDownIcon,
35 ArrowUpIcon: ArrowUpIcon,
36 CalendarIcon: CalendarIcon,
37 CheckIcon: CheckIcon,
38 ChevronDownIcon: ChevronDownIcon,
39 ChevronLeftIcon: ChevronLeftIcon,
40 ChevronRightIcon: ChevronRightIcon,
41 ClearIcon: CrossCircledIcon,
42 CopyIcon: CopyIcon,
43 DisplayIcon: MixerHorizontalIcon,
44 EllipsisIcon: DotsHorizontalIcon,
45 ErrorIcon: CrossCircledIcon,
46 ExpandIcon: SizeIcon,
47 FileTextIcon: FileTextIcon,
48 InfoIcon: InfoCircledIcon,
49 MinusIcon: MinusIcon,
50 MoonIcon: MoonIcon,
51 PlusIcon: PlusIcon,
52 SearchIcon: MagnifyingGlassIcon,
53 ShrinkIcon: MinusIcon,
54 SortAscendingIcon: TextAlignTopIcon,
55 SortDescendingIcon: TextAlignBottomIcon,
56 StopIcon: StopIcon,
57 SuccessIcon: CheckCircledIcon,
58 SunIcon: SunIcon,
59 TableIcon: TableIcon,
60 WarningIcon: ExclamationTriangleIcon,
61 XIcon: Cross1Icon
62};
63
64export function Providers({ children }: { children: React.ReactNode }) {
65 return <Theme icons={{ components: radixIcons }}>{children}</Theme>;
66}

Three keys are not in the map, because radix has no equivalent: FilterIcon, PanelLeftIcon and CoPilotIcon. All three were in-house SVGs before. Radix MagicWandIcon is the nearest stand-in for CoPilotIcon if you want one.

You do not have to take the whole map. A partial map changes only the keys it names.

6. Register from a client component

An override map is an object of functions, and a function cannot cross the boundary from a React Server Component to a Client Component. If your <Theme> sits directly in a server layout today, move it into a providers.tsx file marked 'use client', as shown above.

This applies to any runtime icon override.

Next

See Icons for the full set, the override API, and what each key draws.