Concepts
SVG imports
Import any SVG file, from your project or an icon set, as an inline component with ?component, optimised at build time and styled with CSS.
#Concept
With the Lumiose Vite plugin, an .svg file imported with ?component is a component. The build
optimises the file with SVGO and compiles it into a static template, so
rendering an icon clones markup, on the server and in the browser alike. The icon is
inline: its paths are part of the page's HTML, CSS reaches them, and nothing is fetched.
The query is the switch, so it works the same for your files and for packages:
import House from "lucide-static/icons/house.svg?component" (see
Icons with Lucide). Without it, an .svg import is what Vite makes of it: a URL by default.
Props override the file. The file's root attributes (
width,height,viewBox,fill,stroke,class, …) are defaults; any<svg>prop you pass replaces the matching one:class,style,width,height,fill,aria-*, events andref. The defaults other thanclassandstylestay in the cloned template, so rendering an icon only sets the props you pass.Colour. Icons drawn with
currentColorfollow the text colour (colorin CSS). For icons with a hard-coded colour, setlumiose({ svg: { currentColor: true } })to replace every colour withcurrentColor, or passfill/strokeprops.Accessibility. Without
title,aria-labeloraria-labelledby, an icon is decorative and getsaria-hidden="true".title="Home"renders<title>as the first child, withrole="img"andaria-labelledbypointing at it. The id comes from the file and the title text, so the server and the client agree without a counter. The file's own<title>is dropped: the prop is the label.What the build keeps. An allowlist, whatever the SVGO config, so an icon file can never run script or act as HTML on your page:
- Elements: the SVG ones, spelled as SVG spells them (
linearGradient, notlineargradient):svg,g,defs,symbol,use,switch,a,style, the shapes (path,rect,circle,ellipse,line,polyline,polygon), text (text,tspan,textPath),image, paint servers,clipPath,mask,marker,filterand everyfe*primitive, and the SMIL animationsanimate,animateMotion,animateTransform,setandmpath. Anything else is dropped with its content:<script>,<foreignObject>(and the HTML in it), editor elements (sodipodi:*,<rdf>), unknown or invalid names. - Attributes: the SVG presentation, geometry, link, text, gradient, filter and animation
attributes,
id,class,style,role,tabindex,aria-*anddata-*, as plain string attributes. Event handlers (onload), DOM properties (innerHTML) and attributes of other namespaces are dropped; so arejavascript:URLs. - Animations that target a link (
href), a handler (on…) orstyle, in any letter case, or whosevalues/from/to/byhold ajavascript:URL, are dropped. <style>text keeps its CSS, with<and&written as CSS escapes (\3c,\26): in a page, an inline SVG's<style>is parsed as markup, where they could open a tag.- Elements nested more than 128 levels deep make the file an error.
Comments and editor metadata go too, and redundant markup when optimising. Ids get a per-file prefix, and so do the references to them (
href="#id",url(#id),<style>selectors); class names get it too when the file has a<style>. Icons never clash with each other or with the page.- Elements: the SVG ones, spelled as SVG spells them (
Optimisation. By default SVGO runs its
preset-defaultplusconvertStyleToAttrs(fewerstyleattributes, for strict CSPs). Pass your own SVGO config asoptimizeto choose the plugins ({ plugins: [...] },multipass,floatPrecision), orfalseto skip SVGO.Query switches.
./icon.svg?componentis the component. The rest are Vite's:./icon.svgand?urlare the asset URL (or adata:URL for small files),?inlinealways adata:URL and?rawthe file's text. A library that imports its own.svgfiles gets the URL it expects.lumiose({ svg: false })leaves?componentto Vite too.
Inline is a deliberate choice over sprites (<use href="#id">): with the compression every
server applies, a page of repeated inline icons transfers about as many bytes as a sprite, and
inline icons render faster and can be styled and animated part by part.
#API
| Import / option | Gives |
|---|---|
import Icon from "./icon.svg?component" |
(props: JSX.SVGProps<SVGSVGElement> & { title?: string }) |
import Icon from "pkg/icon.svg?component" |
The same, from a package |
import url from "./icon.svg" |
The asset URL (string, Vite; typed by vite/client) |
import url from "./icon.svg?url" |
The asset URL (string, Vite) |
import data from "./icon.svg?inline" |
A data: URL (string, Vite; typed by vite/client) |
import text from "./icon.svg?raw" |
The file's text (string, Vite) |
lumiose({ svg: { optimize: config } }) |
Optimise with your SVGO config instead of Lumiose's |
lumiose({ svg: { optimize: false } }) |
Skip SVGO (sanitising and id prefixes still run) |
lumiose({ svg: { currentColor: true } }) |
Replace every colour with currentColor |
lumiose({ svg: false }) |
.svg imports stay Vite's URLs |
The types come with lumiose/env ("types": ["lumiose/env"] in tsconfig.json).
#Example
// vite.config.ts: your own SVGO plugins (optional)
import { defineConfig } from "vite";
import lumiose from "@lumiose/vite";
export default defineConfig({
plugins: [
lumiose({
svg: { optimize: { multipass: true, plugins: ["preset-default", "removeDimensions"] } },
}),
],
});// src/components/Nav.stsx
import Home from "../icons/home.svg?component";
import Search from "../icons/search.svg?component";
import logoUrl from "../icons/logo.svg";
export function Nav() {
return (
<nav>
<img src={logoUrl} alt="Acme" width={96} height={24} />
<a href="/" class="link">
<Home class="icon" /> Home
</a>
<button type="button">
<Search class="icon" title="Search" />
</button>
</nav>
);
}
<style>
.link { color: rebeccapurple; }
.icon { width: 1.25em; height: 1.25em; vertical-align: -0.2em; }
.link:hover .icon { color: crimson; }
</style>The Home icon is decorative (its link has text); the search button has no text, so its icon
gets a title. Both follow the link or button colour through currentColor, and class="icon"
receives the file's scope, so the scoped .icon rule applies.
#Common mistakes
- Forgetting
?component.import Icon from "./icon.svg"is a URL string, so<Icon />is a type error ("cannot be used as a JSX component" withvite/clientin"types", "cannot find module" without it). Add?component.?inlineis not the component either: it is Vite'sdata:URL. - Styling inner paths from a scoped block. The paths belong to the icon component, so only the
root
<svg>(throughclass) carries your component's scope. Use inherited properties (color,fill,stroke,stroke-width) on the root, or:deep(path). - Setting only
width. The file'sheightstays, and the drawing is centred in that box. Pass both, or size icons with CSS as above. - Labelling a decorative icon. An icon next to visible text needs no
title; atitlethere makes screen readers repeat the text. - Hard-coded colours in an icon set. If icons stay black whatever
colorsays, the files hard-code a colour: uselumiose({ svg: { currentColor: true } })for monochrome sets. It runs SVGO'sconvertColorsbefore your own plugins, so with a custom config it still applies. - Expecting
<foreignObject>content. HTML inside an icon is dropped; render it with a component next to the icon. - Removing
viewBoxin a custom SVGO config. WithoutviewBoxan icon no longer scales withwidth/height; SVGO'spreset-defaultkeeps it,removeViewBoxdrops it.
#Related diagnostics
A file that is not well-formed SVG fails the build with its location
(icon.svg: invalid SVG: … at 3:12), as does a file whose root element is not <svg> and one
whose elements nest more than 128 levels deep.
See also: Styles, JSX, reference: @lumiose/vite, reference: JSX types.