Guides
Icons with Lucide
Use Lucide, or any SVG icon set, as inline components compiled at build time, sized and coloured with CSS.
Lucide publishes every icon as an SVG file in the
lucide-static package. Import a file with
?component and it becomes a Lumiose component, the same as an .svg file of your own (see
SVG imports): the build optimises and sanitises it, and the icon renders as
inline markup on the server, in the browser and in islands. Only the icons you import end up in
the build.
#Install
pnpm add -D lucide-staticA dev dependency is enough: the build inlines each imported icon, so neither the client nor the
server build loads lucide-static at runtime.
#Use an icon
The file name is the icon's name on lucide.dev, in kebab case: icons/<name>.svg.
// src/components/Nav.stsx
import House from "lucide-static/icons/house.svg?component";
import Search from "lucide-static/icons/search.svg?component";
export function Nav() {
return (
<nav>
<a href="/" class="link">
<House class="icon" /> Home
</a>
<button type="button" class="link">
<Search class="icon" title="Search" />
</button>
</nav>
);
}
<style>
.link { color: rebeccapurple; }
.icon { width: 1.25em; height: 1.25em; stroke-width: 1.5; }
</style>lumiose/env types every *.svg?component import, from packages too: the props are those of
<svg>, plus title.
#Size, colour and stroke
Lucide's files are 24 × 24, drawn with stroke="currentColor" and stroke-width="2" on the root
<svg>. Every one of these is a default that CSS or a prop replaces:
- Size. Set
widthandheightin CSS, as.icondoes above, or pass both props (width={20} height={20}). Sizing inemmakes an icon follow the text around it. - Colour. The stroke is
currentColor, so the icon takes thecolorof its parent: the link'srebeccapurpleabove, and any:hovercolour you give the link. - Stroke.
stroke-widthin CSS or as a prop (stroke-width={1.5}). The paths inherit it from the root.
class="icon" receives the component's scope, so the scoped .icon rule applies to the icon's
root <svg>.
#Labels
An icon without title, aria-label or aria-labelledby is decorative and gets
aria-hidden="true", which is right next to visible text (the "Home" link). An icon-only control
needs a name: title="Search" on the icon gives it a <title> with role="img", or label the
control itself with <button aria-label="Search"> and keep the icon decorative.
#Other icon sets
Any package that ships its icons as SVG files works the same way: import the file's path with
?component. Monochrome sets drawn with currentColor follow color as Lucide does. For a set
whose files hard-code a colour (fill="#000"), set lumiose({ svg: { currentColor: true } }) in
vite.config.ts, which replaces every colour in ?component imports with currentColor.
#Why not lucide-react or lucide
lucide-react(and the other framework packages) export React components. Lumiose's JSX is not React's, so they do not render in Lumiose.lucide, the vanilla package, builds the icons in the browser at runtime from JavaScript icon data. A?componentimport does that work at build time instead: the server renders the icon, and the client ships no icon code beyond the markup it clones.
#Common mistakes
- Forgetting
?component.import House from "lucide-static/icons/house.svg"is Vite's URL string, so<House />is a type error. Add?component. - Styling Lucide's classes. The files carry
class="lucide lucide-house", which aclassprop replaces. Style your own class, as.iconabove, rather than.lucide. - Setting only
width. The file'sheightof 24 stays and the drawing is centred in that box. Set both, in CSS or as props. - Labelling an icon next to its text. A
titlethere makes screen readers say the label twice. Leave such icons decorative.
See also: SVG imports, Styles, Islands.