svg-sprite-kit
v1.0.0
Published
Bundles SVG icons from a folder into a single SVG sprite file. Zero dependencies, recursive scanning, id isolation, IconName types. React, Vue, Svelte and Angular — through Vite, webpack, a CLI, or Next.js without a plugin.
Maintainers
Readme
Bundles SVG icons from a folder into a single SVG sprite file. Zero dependencies, recursive scanning, id isolation, IconName types. React, Vue, Svelte and Angular — through Vite, webpack, a CLI, or Next.js without a plugin.
Live demo — the icon grid with search, and
every option shown side by side. The page is built by the package itself, from the icons in
demo/icons.
Contents
- Quick start
- Icon in a component — React · Vue 3 · Svelte and SvelteKit · Every icon at once · In plain HTML
- Why this package
- Features
- CLI
- Options —
iconsDirectory·spriteFilePath·symbolId·isolateIds·isolateClasses·currentColor·typesFilePath·demoFilePath·watch·root·onWarn - Vite
- webpack
- Next.js
- Angular
- Core API
- Limitations
- Installation and Node versions
- Types for Vite and webpack
- FAQ
Quick start
npm install -D svg-sprite-kityarn add -D svg-sprite-kitpnpm add -D svg-sprite-kit// vite.config.ts
import { defineConfig } from 'vite';
import { svgSprite } from 'svg-sprite-kit/vite';
export default defineConfig({
plugins: [
svgSprite({
iconsDirectory: './src/icons',
spriteFilePath: './public/sprite.svg'
})
]
});<svg><use href="/sprite.svg#arrow-left" /></svg>A symbol's id is the file path relative to the icons folder: arrow-left.svg → #arrow-left,
ui/nav/close.svg → #ui-nav-close.
That is a complete working config — only iconsDirectory and spriteFilePath are required.
The rest — types, a demo page,
symbolId, currentColor —
are opt-in, and without them no extra files are ever written.
Icon in a component
The sprite is a plain file in public/, so rendering comes down to <use> inside an <svg>.
A wrapper is there only to avoid repeating the path.
React
// src/components/Icon.jsx
export function Icon({ name, size = 24 }) {
return (
<svg width={size} height={size} fill="currentColor" aria-hidden="true">
<use href={`/sprite.svg#${name}`} />
</svg>
);
}import { Icon } from './components/Icon';
export function Toolbar() {
return (
<button className="icon-button">
<Icon name="arrow-left" />
Back
</button>
);
}Color comes from ordinary CSS — .icon-button { color: #2563eb } paints the icon, because
the <svg> carries fill="currentColor". That only works while the icons themselves have no
hard-coded colors; if they do, turn on
currentColor.
With TypeScript the same component gets the icon name checked, provided
typesFilePath is set:
// src/components/Icon.tsx
import type { IconName } from '../icons';
export function Icon({ name, size = 24 }: { name: IconName; size?: number }) {
return (
<svg width={size} height={size} fill="currentColor" aria-hidden="true">
<use href={`/sprite.svg#${name}`} />
</svg>
);
}<Icon name="arow-left" />
// Type '"arow-left"' is not assignable to type 'IconName'. Did you mean '"arrow-left"'?Vue 3
<!-- src/components/Icon.vue -->
<script setup>
defineProps({
name: { type: String, required: true },
size: { type: Number, default: 24 }
});
</script>
<template>
<svg :width="size" :height="size" aria-hidden="true">
<use :href="`/sprite.svg#${name}`" />
</svg>
</template>
<style scoped>
svg {
fill: currentColor;
}
</style><script setup>
import Icon from './components/Icon.vue';
</script>
<template>
<button class="icon-button">
<Icon name="arrow-left" />
Back
</button>
</template>With TypeScript the icon name is checked at compile time just the same:
<script setup lang="ts">
import type { IconName } from '../icons';
defineProps<{ name: IconName; size?: number }>();
</script>Svelte and SvelteKit
SvelteKit serves static files from static/, not from public/ — that is the only thing
setting this config apart from the quick start. The link stays the same, /sprite.svg.
In a Svelte project without Kit static files live in public/, and the quick-start config
applies verbatim.
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { svgSprite } from 'svg-sprite-kit/vite';
export default defineConfig({
plugins: [
sveltekit(),
svgSprite({
iconsDirectory: './src/icons',
spriteFilePath: './static/sprite.svg', // not public/
typesFilePath: './src/lib/icons.ts'
})
]
});<!-- src/lib/components/Icon.svelte -->
<script lang="ts">
import type { SVGAttributes } from 'svelte/elements';
import type { IconName } from '$lib/icons';
interface Props extends SVGAttributes<SVGSVGElement> {
name: IconName;
size?: number;
}
let { name, size = 24, ...rest }: Props = $props();
</script>
<svg width={size} height={size} aria-hidden="true" {...rest}>
<use href={`/sprite.svg#${name}`} />
</svg>
<style>
svg {
fill: currentColor;
}
</style><script>
import Icon from '$lib/components/Icon.svelte';
</script>
<button class="icon-button">
<Icon name="arrow-left" />
Back
</button>The example uses Svelte 5 runes; on Svelte 4 the same lines read export let name;
and {...$$restProps}. Extending SVGAttributes<SVGSVGElement> is what makes the rest props
work: without it <Icon name="plus" class="big" /> fails type checking.
If the site lives under a subpath (config.kit.paths.base), the absolute /sprite.svg will miss
the file — then import { base } from '$app/paths' and href={`${base}/sprite.svg#${name}`}.
Every icon at once
iconNames is a plain array, so a gallery is a matter of mapping over it. Handy for a design
system page or a Storybook story:
import { iconNames } from '../icons';
export function IconGallery() {
return (
<div className="gallery">
{iconNames.map((name) => (
<figure key={name}>
<Icon name={name} size={32} />
<figcaption>{name}</figcaption>
</figure>
))}
</div>
);
}In plain HTML
<svg width="24" height="24" fill="currentColor" aria-hidden="true">
<use href="/sprite.svg#arrow-left"></use>
</svg>Why this package
| Package | Direct dependencies | Packages in the tree | On disk | | ---------------------------- | ------------------- | -------------------- | ---------- | | svg-sprite-kit | 0 | 1 | 159 KB | | vite-plugin-svg-spritemap | 5 | 24 | 8.4 MB | | svg-sprite-loader | 8 | 210 | 11.0 MB | | svg-spritemap-webpack-plugin | 9 (+4 peer) | 23 | 11.7 MB | | svg-sprite | 17 | 93 | 13.8 MB | | vite-plugin-svg-icons | 8 | 227 | 16.6 MB |
The size gap is neither an accident nor an optimization. The core imports no bundler at all, so
the package has neither dependencies nor peerDependencies. The Vite and webpack adapters
describe the bundler types structurally and ship as separate entry points, Next.js needs only the
core, and SVG is parsed with strings instead of a DOM parser.
The second difference is reach. The other packages are tied to one bundler; here Vite, webpack, Next.js and a CLI are covered by a single install:
| | svg-sprite-kit | vite-plugin-svg-spritemap | svg-spritemap-webpack-plugin | svg-sprite | | -------------------- | --------------------- | ------------------------- | ---------------------------- | ---------- | | Vite | yes | yes | no | no | | webpack | yes | no | yes | no | | Next.js | yes, without a plugin | no | no | no | | CLI | yes | no | no | yes | | Minimum Node version | 18 | 20.19 | 20.11 | 12 |
Features
- Recursive traversal, across several folders at once. Nesting is reflected in the
id, your own and vendor icons end up in one file, and the build order is the same on every machine. idand class isolation inside icons. Gradients, masks and<style>rules that share names no longer clash in the sprite's shared document — and clash they do, constantly, because Illustrator gives every icon the class.st0.- A typo in an icon name is caught by the compiler. The generated module exports the type
IconNameand the listiconNames, which is also what you map over to render a catalog. - A demo page with every icon. Self-contained HTML with search and click-to-copy
#id— open it and see what the set actually holds. - Works without a bundler too. The CLI covers projects where the build config is out of reach.
CLI
npx svg-sprite-kit ./src/icons --output ./public/sprite.svgBundles SVG icons from a folder into a single sprite file.
Usage:
svg-sprite-kit <folder...> --output <file> [options]
Options:
-o, --output <file> path to the generated sprite (required)
-t, --types <file> generate a module holding the IconName type
(an extension other than .ts/.mts — plain JS, no type)
-d, --demo <file> generate a demo page listing every icon
--symbol-id <template> symbol id template, defaults to [dir]-[name]
--no-isolate-ids do not prefix ids inside icons
--no-isolate-classes do not prefix classes from <style> inside icons
--current-color replace hard-coded colors with currentColor
-w, --watch watch for changes and rebuild
-h, --help show this help
-v, --version show the versionSeveral folders may be given. A flag's value is written after a space or after =.
The call is rarely typed by hand — usually it hangs off an npm script, and then the sprite rebuilds itself before every start and every build:
{
"scripts": {
"prestart": "svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/icons.ts",
"prebuild": "svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/icons.ts"
}
}The pre prefix is all it takes: npm start and npm run build invoke prestart and prebuild
themselves, and the commands you type stay the same. This is how the package plugs into projects
where the bundler config is out of reach — Angular, for one.
Options
The same set is accepted by svgSprite() for Vite, SvgSpritePlugin for webpack and
createSpriteGenerator() for Next.js — the adapters add no fields of their own.
iconsDirectory — where icons come from
iconsDirectory: './src/img/icons'; // one folder
iconsDirectory: ['./src/img/icons', './src/vendor-icons']; // severalThe traversal is recursive. An array is what you want when your own icons sit apart from vendor ones — they end up in a single file. A missing folder is not an error: a warning goes out and the build carries on.
spriteFilePath — where the sprite goes
spriteFilePath: './public/sprite.svg'; // Vite, Next.js
spriteFilePath: './static/sprite.svg'; // SvelteKitFolders along the path are created for you. The file has to land in the static directory, or the
browser will never get it: /sprite.svg resolves from the server root.
symbolId — naming the symbols
[name] is the file name without .svg, [dir] is the subfolder path with separators turned
into hyphens.
| File | '[dir]-[name]' (default) | 'icon-[name]' |
| ------------------ | -------------------------- | --------------- |
| arrow.svg | #arrow | #icon-arrow |
| ui/nav/close.svg | #ui-nav-close | #icon-close |
For files at the root [dir] is empty and the dangling hyphen is trimmed. When two files produce
the same id, the first one in traversal order wins and the rest are skipped with a warning.
That order depends neither on the filesystem nor on when files were created: folders come in the
order listed, files precede subfolders, and subfolders go alphabetically.
isolateIds — fixing id clashes inside icons
isolateIds: true; // defaultEvery icon lives in one shared document, so identical gradient and mask ids collide: the browser
applies the first one to both icons. Internal ids get prefixed with the symbol name, and
references to them are rewritten to match:
before: <linearGradient id="a"/><path fill="url(#a)"/>
after: <linearGradient id="sun-a"/><path fill="url(#sun-a)"/>References to ids that the icon does not declare itself are left alone. Setting false makes
sense only if something outside points at those internal ids.
isolateClasses — the same for classes from <style>
isolateClasses: true; // defaultThe same trouble, only sneakier: CSS inside a <symbol> is not scoped to that symbol and
applies to the whole sprite document. Illustrator names every icon's class .st0, so without
a prefix the last rule in the file wins:
before: <symbol id="red"><style>.st0{fill:#E00000}</style><circle class="st0"/></symbol>
<symbol id="blue"><style>.st0{fill:#0000E0}</style><rect class="st0"/></symbol>
both icons end up in the color of the last one
after: <symbol id="red"><style>.red-st0{fill:#E00000}</style><circle class="red-st0"/></symbol>
<symbol id="blue"><style>.blue-st0{fill:#0000E0}</style><rect class="blue-st0"/></symbol>Only classes with a matching rule in the icon's own <style> get renamed. A class without a rule
is a hook for the page's stylesheet and must stay untouched:
<path class="st0 icon-arrow"/> → <path class="menu-st0 icon-arrow"/>currentColor — inheriting the text color
currentColor: false; // defaultbefore: <path fill="#333333"/><path fill="none" stroke="#000"/>
after: <path fill="currentColor"/><path fill="none" stroke="currentColor"/>.button:hover .icon {
color: red;
}Off by default, because such a replacement destroys multi-color icons. What survives untouched:
fill="none" and stroke="none" (otherwise outlined icons would be filled solid), url(#grad)
references (otherwise gradients would vanish) and stop-color (otherwise a gradient would
collapse into one color). Colors in a style attribute and inside a <style> block are handled
alongside attributes.
typesFilePath — a module with icon names
typesFilePath: './src/icons.ts';// src/icons.ts
// Generated by svg-sprite-kit. Do not edit by hand.
export const iconNames = ['arrow-left', 'ui-nav-close'] as const;
export type IconName = (typeof iconNames)[number];Gives autocompletion and catches a mistyped icon name at compile time — the React section shows the check in action. Without the option no file is written.
In a project without TypeScript use the .js extension: you get a plain ESM module with no
type but the same list of names.
typesFilePath: './src/icons.js';// src/icons.js
export const iconNames = ['arrow-left', 'ui-nav-close'];The format follows the extension: .ts and .mts get TypeScript, everything else plain JS.
The module is always ESM, so webpack and Vite pick up the export in a .js file regardless of
the type field in package.json. If Node.js itself reads the file, reach for .mjs instead:
without "type": "module" it treats .js as CommonJS and trips over export.
demoFilePath — a page listing every icon
demoFilePath: './public/icons.html';Self-contained HTML: a grid of icons, search by name, click to copy the #id. It pulls in no
external resources. The link to the sprite is resolved relative to the page itself, so keep the
two side by side. Opens at an address like http://localhost:5173/icons.html.
watch — whether to watch for changes
watch: true; // defaultIn the Vite and webpack adapters it cancels the subscription to the bundler's watcher; in the core
it turns generator.watch() into a no-op. Watching is done by the bundler's own watcher rather
than by this package, which is why server.watch and watchOptions are never ignored. For Vite
and webpack keep the default: a production build sets up no subscription anyway.
root — base for relative paths
root: process.cwd(); // defaulticonsDirectory, spriteFilePath, typesFilePath and demoFilePath are resolved against it.
Worth touching only when the build does not run from the project root — in a monorepo, say.
onWarn — where warnings go
onWarn: (message) => myLogger.warn(message);Defaults to console.warn with the [svg-sprite-kit] prefix; the adapters route messages into
the Vite or webpack logger instead. Warnings come from a missing folder, a file without a root
<svg> and an id collision.
The whole configuration
svgSprite({
iconsDirectory: ['./src/img/icons'], // a one-element array is just a string
spriteFilePath: './public/sprite.svg',
typesFilePath: './src/icons.ts',
demoFilePath: './public/icons.html',
symbolId: '[dir]-[name]', // the default, can be omitted
isolateIds: true, // the default, can be omitted
watch: true // the default, can be omitted
});And the same plugin with everything optional stripped away:
svgSprite({
iconsDirectory: './src/img/icons',
spriteFilePath: './public/sprite.svg'
});Vite
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { svgSprite } from 'svg-sprite-kit/vite';
export default defineConfig({
plugins: [
react(),
svgSprite({
iconsDirectory: './src/img/icons',
spriteFilePath: './public/sprite.svg',
typesFilePath: './src/icons.ts',
demoFilePath: './public/icons.html'
})
]
});This is the extended form: typesFilePath and demoFilePath are optional. The plugin does not
care about the framework — see Vue and Svelte under
"Icon in a component".
The sprite is built on buildStart. In dev mode the plugin subscribes to Vite's own watcher and
sends full-reload after an icon changes — but only when the sprite's content actually changed.
webpack
// webpack.config.js
import { SvgSpritePlugin } from 'svg-sprite-kit/webpack';
export default {
plugins: [
new SvgSpritePlugin({
iconsDirectory: './src/icons',
spriteFilePath: './public/sprite.svg'
})
]
};The sprite is built on beforeCompile. Icon folders are registered in contextDependencies, so
in watch mode webpack's own watcher tracks them — the plugin sets up no watcher of its own and
does not ignore watchOptions.
Next.js
No plugin needed: the config file runs before both next dev and next build, under either
bundler — webpack and Turbopack alike.
// next.config.mjs
import { createSpriteGenerator } from 'svg-sprite-kit/generator';
const sprite = createSpriteGenerator({
iconsDirectory: './icons',
spriteFilePath: './public/sprite.svg',
currentColor: true,
watch: true
});
sprite.generate();
sprite.watch();
export default {};The options are the same as the adapters' — one object fits all three. watch: false turns the
sprite.watch() call into a no-op, so the mode is switched in the options rather than around the
call. currentColor: true is here for the component from the React section; on a
multi-color icon set you would drop that option instead.
Calling watch() does not get in the way of next build even if you leave watch: true: the
watcher is marked unref() and does not keep the process alive. The option saves a file
descriptor, nothing more.
Unlike Vite, the page will not reload by itself: a Next config has no channel to the dev server.
watch() keeps the sprite file current, but the tab has to be refreshed by hand.
Angular
There is no plugin: the sprite is built through the CLI from npm scripts, which makes the setup independent of the Angular version and of the builder.
{
"scripts": {
"prestart": "svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/app/icons.ts --current-color",
"prebuild": "svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/app/icons.ts --current-color"
}
}npm runs prestart and prebuild itself, right before ng serve and ng build — the commands
you type stay the same. The static directory is public/ on Angular 18 and newer and
src/assets/ before that; the -o path moves with it. The --current-color flag is there for
the fill: currentColor in the component below — what it actually does.
// src/app/icon.component.ts
import { Component, input } from '@angular/core';
import type { IconName } from './icons';
@Component({
selector: 'app-icon',
template: `
<svg
[attr.width]="size()"
[attr.height]="size()"
aria-hidden="true"
focusable="false"
>
<use [attr.href]="'/sprite.svg#' + name()" />
</svg>
`,
styles: `
svg {
fill: currentColor;
}
`
})
export class IconComponent {
readonly name = input.required<IconName>();
readonly size = input(24);
}<button class="icon-button">
<app-icon name="arrow-left" />
Back
</button>Three things Angular will remind you about on its own:
[attr.href], nothref="{{ ... }}". SVG attributes have no matching DOM properties, so plain interpolation does nothing here — the binding has to go throughattr..focusable="false". Without it the icon catches focus during tab navigation.strictTemplatesintsconfig.json. Icon names are only checked in templates with it on; otherwiseinput.required<IconName>()has no effect.
To keep icons up to date while you work, run a second process alongside ng serve:
npx svg-sprite-kit ./src/icons -o ./public/sprite.svg -t ./src/app/icons.ts --current-color --watchThe sprite will stay current, but the tab has to be refreshed by hand — a separate process has no channel to the dev server, same as the Next.js config.
Core API
import { createSpriteGenerator } from 'svg-sprite-kit';createSpriteGenerator(options)
| Option | Type | Default | Description |
| ---------------- | ----------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------- |
| iconsDirectory | string \| readonly string[] | — | Folder holding the source icons, or several folders |
| spriteFilePath | string | — | Path to the generated sprite |
| root | string | process.cwd() | Base for resolving relative paths |
| symbolId | string | '[dir]-[name]' | Symbol id template: [dir] is the subfolder path, [name] the file name |
| isolateIds | boolean | true | Whether to prefix ids inside icons with the symbol name |
| isolateClasses | boolean | true | Whether to prefix classes from <style> inside icons |
| currentColor | boolean | false | Whether to replace hard-coded colors with currentColor |
| typesFilePath | string | — | Path to the generated module with IconName. An extension other than .ts/.mts yields plain JS without the type |
| demoFilePath | string | — | Path to the generated demo page |
| watch | boolean | true | Whether watching the icons is allowed |
| onWarn | (message: string) => void | console.warn | Where warnings go |
Each option is covered with examples under Options. The same set is accepted by
svgSprite() from svg-sprite-kit/vite and SvgSpritePlugin from svg-sprite-kit/webpack —
the adapters have no options type of their own.
Returns an object:
| Member | Description |
| ------------------------------ | ----------------------------------------------------------------------------------- |
| generate(): boolean | Builds and writes the sprite, types and demo. true when at least one file changed |
| watch(onChange?): () => void | Watches the folders. Returns a stop function |
| isIconFile(path): boolean | Whether the path belongs to a source icon |
| iconsFolders | Absolute paths to the icon folders |
| spriteFile | Absolute path to the sprite file |
generate() writes a file only when its content differs from what is already there. The guard
against endless regeneration rests on that: a watcher event that leaves the sprite unchanged
triggers no reload and spawns no follow-up event.
Repeated watch() calls with the same set of paths share one set of watchers — which matters for
Next.js, as it loads the config several times within a process. Every call returns its own
close(); the watchers shut down once the last one is closed.
A missing icons folder is not an error: generate() and watch() emit a warning and return.
Limitations
- SVG parsing is string-based, without a DOM parser. A nested
<svg>inside an icon will not be handled correctly. That is the price of zero dependencies. - Not every attribute of the root
<svg>survives.viewBoxand the presentation attributes move onto the<symbol>—fill,stroke, thestroke-*settings,opacity,color,fill-rule,clip-rule,vector-effect— so icon sets that keep their styling on the root tag, Lucide and Feather among them, come out looking as drawn. Dropped on purpose:widthandheight(on a symbol they define its viewport and would break scaling through<use>),classandstyle(they drag the page's stylesheet into the sprite), andid. - SVGO is not built in. Optimize icons as a separate step — why.
- Symbol
idcollisions. Two files with the samesymbolIdcannot coexist: the first one in traversal order stays, the rest are skipped with a warning.
Installation and Node versions
Node.js 18 or newer is required.
The package ships as ESM only. On Node.js 20.19+ and 22.12+ it can also be pulled in through
require() — those versions load ESM synchronously. On Node.js 18 require() will not work:
the config importing the package has to be a module. vite.config.ts (Vite loads it as ESM) and
next.config.mjs qualify; next.config.js with require() does not.
Types for Vite and webpack
The adapters describe the bundler types themselves so that neither bundler has to be a dependency. Normally they assign to the real types without a single cast. If a bundler version drifts away from them, a targeted cast helps:
plugins: [svgSprite({ ... }) as unknown as PluginOption];FAQ
I only use Vite. Do I ship the webpack adapter along with it?
Only as 8.8 KB sitting on disk — 5% of the installed package. Nothing else: exports declares
separate entry points, and ESM loads a file when something imports it, so dist/webpack.js is
never read, never type-checked, and never bundled. The adapter does not drag in webpack either —
the bundler types are declared structurally, which is why this package has no dependencies
and no peerDependencies at all.
Does any of this end up in my client bundle?
No. The package runs in Node at build time and writes files. The only thing a browser downloads is the sprite itself, as a plain static file.
Why does the whole page reload when I edit an icon, instead of hot-swapping it?
The sprite is a static file, not a module in the bundler's graph, so there is nothing to hot-swap:
the plugin sends full-reload instead, and only when the sprite's content actually changed.
Can I inline the sprite into the DOM instead of fetching it?
Not through an option, but nothing stops you from doing it by hand — the sprite is an ordinary
file. In Vite, import sprite from '../static/sprite.svg?raw', inject it once in the root layout,
and reference symbols as href="#arrow-left" without a path. You trade one request for a sprite
that is embedded in the HTML and no longer cached on its own; on a large icon set that is usually
the worse deal.
<img src="/sprite.svg#arrow-left"> shows nothing. Why?
<symbol> is reachable only through <use> inside an SVG document. <img> and CSS url() need
a fragment that defines a viewport — a <view> element — and this package does not generate one.
Use <use>, or embed the icon as its own file.
My icons ignore the CSS color around them.
Their colors are still hard-coded in the source files. Turn on
currentColor — with the caveat that it flattens
multi-color icons into a single color.
Why is SVGO not built in?
Because it would end the package's zero-dependency promise: SVGO and its own dependencies weigh
more than everything shipped here. Optimizing icons is also a separate concern with its own
schedule — it belongs to the moment icons enter the repository, not to every rebuild. Run it as
its own step, for example npx svgo --folder ./src/icons, and this package will bundle whatever
it finds.
Does it work with Svelte, Angular, Astro, Nuxt, …?
Anything built on Vite or webpack — the adapters know nothing about frameworks and only need a place in the plugin list. Where the bundler config is out of reach, an npm script running the CLI covers it; see Angular.
