[ Interface ]
@sdxc/icons
Lucide icons as remix/component components, one tree-shakeable export per icon
- Used by
- uptime, auth-saas, reader, blog
- Source
- packages/icons
Lucide icons as remix/component components, one tree-shakeable export per
icon.
The icons ship prebuilt, so each one is a plain import and a bundler keeps only the ones you name.
Props and defaults match lucide-react.
Installation
npm add @sdxc/icons
The icons render through remix/component, from remix.
Usage
Render an icon
Every icon in the Lucide catalog is exported as
<PascalCaseName>Icon: heart is HeartIcon, circle-alert is CircleAlertIcon.
import { HeartIcon } from "@sdxc/icons";
function LikeButton() {
return () => (
<button>
<HeartIcon size={16} />
Like
</button>
);
}
Set size, color and stroke
import { CircleAlertIcon } from "@sdxc/icons";
function ErrorBanner() {
return () => <CircleAlertIcon size={32} color="crimson" strokeWidth={1.5} className="banner" />;
}
color defaults to currentColor, so an icon follows the surrounding text color. className is
added to the lucide and lucide-<name> classes every icon carries.
Give an icon an accessible name
An icon renders aria-hidden="true", which suits an icon beside visible text. Pass any aria-*
attribute or a role and the icon is exposed with that name instead:
import { TrashIcon } from "@sdxc/icons";
function DeleteButton() {
return () => (
<button>
<TrashIcon aria-label="Delete" />
</button>
);
}
Render an icon chosen at runtime
When the icon comes from data, render <Icon name />. name is typed as the union of every icon
name, so a misspelling is a compile error:
import type { IconName } from "@sdxc/icons";
import type { Handle } from "remix/component";
import { Icon } from "@sdxc/icons";
interface NavLinkProps {
icon: IconName;
label: string;
href: string;
}
function NavLink({ props }: Handle<NavLinkProps>) {
return () => (
<a href={props.href}>
<Icon name={props.icon} size={18} />
{props.label}
</a>
);
}
API
<PascalCaseName>Icon
One component per Lucide icon, each in its own module. It renders an <svg> with a 24x24 viewBox,
no fill and round line caps, and accepts LucideProps:
size: width and height, default24.color: stroke color, default"currentColor".strokeWidth: stroke width in viewBox units, default2.absoluteStrokeWidth: scalesstrokeWidthagainstsize, so the stroke keeps the same visual weight at any size.
Every other <svg> prop (className, style, aria-*, mix) reaches the root element.
Children render inside the <svg> after the icon's own elements, and an icon with children skips
the aria-hidden default.
Icon
Renders the icon whose kebab-case name is name, forwarding the remaining LucideProps. It reaches
the whole catalog's data, so prefer the specific <XyzIcon> import wherever the icon is known.
<Icon name="heart" size={16} color="red" />
iconExportNames
An object keyed by every icon's kebab-case name ("circle-alert"); Object.keys(iconExportNames)
lists the full catalog.
createLucideIcon(iconName: string, iconNode: IconNode)
Builds an icon component from a kebab-case name and SVG node data, with the same props, defaults and classes as every shipped icon.
let SquareOutlineIcon = createLucideIcon("square-outline", [["path", { d: "M4 4h16v16H4z" }]]);
Types
type IconNode = ReadonlyArray<readonly [tag: string, attrs: Record<string, string | number>]>;
interface LucideProps extends Props<"svg"> {
size?: number | string;
color?: string;
strokeWidth?: number | string;
absoluteStrokeWidth?: boolean;
}
interface IconProps extends LucideProps {
name: IconName;
}
type IconName = keyof typeof iconExportNames;
Pattern: Building An Icon Picker
iconExportNames lists the catalog and <Icon /> draws any entry of it:
import type { IconName } from "@sdxc/icons";
import type { Handle } from "remix/component";
import { Icon, iconExportNames } from "@sdxc/icons";
let names = Object.keys(iconExportNames) as IconName[];
function IconPicker({ props }: Handle<{ query: string }>) {
return () => (
<ul>
{names
.filter((name) => name.includes(props.query))
.slice(0, 60)
.map((name) => (
<li>
<button name="icon" value={name}>
<Icon name={name} size={20} />
{name}
</button>
</li>
))}
</ul>
);
}
Pattern: Validating A Stored Icon Name
An icon name read from a form or a database is a plain string. Validate it against the catalog with
remix/data-schema and it becomes an IconName that
<Icon /> accepts:
import type { IconName } from "@sdxc/icons";
import { Icon, iconExportNames } from "@sdxc/icons";
import { enum_, parseSafe } from "remix/data-schema";
let IconNameSchema = enum_(Object.keys(iconExportNames) as [IconName, ...IconName[]]);
function renderStoredIcon(value: unknown) {
let result = parseSafe(IconNameSchema, value);
if (!result.success) return null;
return <Icon name={result.value} size={20} />;
}
Pattern: Keeping Icons Out Of The Client Bundle
Server-rendered components never ship to the browser, so <Icon name /> costs the page nothing
there. Inside a clientEntry component, which the browser loads, import the specific icon so the
bundle carries one icon's data:
import type { Handle } from "remix/component";
import { HeartIcon } from "@sdxc/icons";
import { clientEntry, on } from "remix/component";
export let LikeButton = clientEntry(
"/assets/like-button.js#LikeButton",
function LikeButton(handle: Handle<{ label: string }>) {
let liked = false;
return () => (
<button
mix={[
on("click", () => {
liked = !liked;
handle.update();
}),
]}
>
<HeartIcon size={16} color={liked ? "crimson" : "currentColor"} />
{handle.props.label}
</button>
);
},
);
Pattern: Adding An Icon Lucide Doesn't Ship
A brand mark or product glyph drawn on Lucide's grid (24x24 viewBox, stroke width 2, no fill)
becomes a component that takes size, color and strokeWidth like the rest:
import { createLucideIcon, HeartIcon } from "@sdxc/icons";
let SparkIcon = createLucideIcon("spark", [
["path", { d: "M12 2v6" }],
["path", { d: "m16 6-4 4-4-4" }],
["circle", { cx: "12", cy: "16", r: "4" }],
]);
function Toolbar() {
return () => (
<nav>
<SparkIcon size={20} absoluteStrokeWidth />
<HeartIcon size={20} absoluteStrokeWidth />
</nav>
);
}
The component carries the lucide and lucide-spark classes, so stylesheets targeting icons
reach it too.