vite-plugin-svg-symbols
Version:
Vite plugin for creating SVG symbol sprite
518 lines (456 loc) • 12.9 kB
Markdown
# vite-plugin-svg-symbols
[](https://raw.githubusercontent.com/werty1001/vite-plugin-svg-symbols/main/LICENSE) [](https://www.npmjs.com/package/vite-plugin-svg-symbols) [](https://www.npmjs.com/package/vite-plugin-svg-symbols) 
> Vite plugin for creating SVG symbol sprite 🧞♂️
### Features
* Create a single sprite from SVG files
* Use a sprite as a static file for JS bundle optimization
* The production build includes only the icons that are actually used
* Support mass import or by one
* Compatible with modern frameworks: [Vue](#vue) / [Svelte](#svelte) / [React](#react--solid--preact) / [Solid](#react--solid--preact) / [Preact](#react--solid--preact)
### Navigation
* [Getting Started](#getting-started)
* [Usage](#usage)
* [Examples](#examples)
* [Options](#options)
<p align="center">
<a href="#vite-plugin-svg-symbols"><img align="center" src="https://werty1001.github.io/sep.svg" alt=""></a>
</p>
## Getting Started
**1.** Install using npm:
```bash
npm i vite-plugin-svg-symbols -D
```
**2.** Add to your `vite.config.ts`:
```js
import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'node:url';
import useSvgSymbols from 'vite-plugin-svg-symbols'; // ← [1] import plugin
export default defineConfig({
plugins: [
useSvgSymbols(), // ← [2] use plugin
],
resolve: {
alias: {
'@icons': fileURLToPath(new URL('./src/icons', import.meta.url)), // ← [3] add alias
},
},
});
```
**3.** Add to your `vite-env.d.ts` or `env.d.ts`:
```ts
/// <reference types="vite-plugin-svg-symbols/client" />
```
> [!TIP]
> If your project doesn't include an `env.d.ts` file, you can specify the type reference in `tsconfig.app.json`:
> ```json
> "compilerOptions": {
> "types": ["vite-plugin-svg-symbols/client"]
> }
> ```
**4.** Finally, add the `vpss` command **before** type checking in `package.json`:
> [!IMPORTANT]
> **The `vpss` command generates types for icons to avoid errors such as:**
>
> `error TS2614: Module 'svg:symbols@icons' has no exported member ...`
```json
"scripts": {
"build": "vpss && tsc -b && vite build"
}
```
Another option is to run type checking **after** the build process; since types are generated during the build, no extra command is required:
```json
"scripts": {
"build": "vite build && tsc -b"
}
```
<p align="center">
<a href="#vite-plugin-svg-symbols"><img align="center" src="https://werty1001.github.io/sep.svg" alt=""></a>
</p>
## Usage
For example, you have `src/icons/cat.svg` and `src/icons/dog.svg` and **@icons** alias in `vite.config.ts`
#### Plain import
```js
import dogIcon from '@icons/dog.svg?symbol'; // import icon as symbol href
```
#### Mass import
```js
import { dogIcon, catIcon } from 'svg:symbols@icons'; // import two icons as symbol hrefs
```
#### Icon packages
You can add alias for library, for example **@lucide** ([lucide](https://www.npmjs.com/package/lucide-static)):
```js
// vite.config.ts
alias: {
'@lucide': fileURLToPath(new URL('./node_modules/lucide-static/icons/', import.meta.url)),
},
```
Then import icons from **lucide** package:
```js
import bananaIcon from '@lucide/banana.svg?symbol';
```
```js
import { appleIcon, bananaIcon } from 'svg:symbols@lucide';
```
#### Import names
By default, icon names are defined in camel case with the `Icon` suffix:
```js
banana.svg → bananaIcon
a-arrow-up.svg → aArrowUpIcon
arrow-down-a-z.svg → arrowDownAZIcon
arrow-down-1-0.svg → arrowDown10Icon
// subdir
home/foo.svg → homeFooIcon
// when same names
arrow-down-0-1.svg → arrowDown01Icon
arrow-down-01.svg → arrowDown01Icon2 (because arrowDown01Icon already exist)
```
<p align="center">
<a href="#vite-plugin-svg-symbols"><img align="center" src="https://werty1001.github.io/sep.svg" alt=""></a>
</p>
## Examples
When you have `src/icons/cat.svg` and `src/icons/dog.svg` and **@icons** alias in `vite.config.ts`
> [Vue](#vue) / [Svelte](#svelte) / [React](#react--solid--preact) / [Solid](#react--solid--preact) / [Preact](#react--solid--preact)
### Vue
Plain import:
```vue
<script setup lang="ts">
import dogIcon from '@icons/dog.svg?symbol';
</script>
<template>
<svg aria-hidden="true">
<use :href="dogIcon" /> // use dogIcon from script
</svg>
<svg aria-hidden="true">
<use href="@icons/cat.svg?symbol" /> // or import icon directly
</svg>
<!-- Don't do this -->
<svg aria-hidden="true">
<use :href="'@icons/cat.svg?symbol'" /> // ⚠️ dynamic path not working
</svg>
<!-- :( -->
</template>
```
Mass import:
```vue
<script setup lang="ts">
import { dogIcon, catIcon } from 'svg:symbols@icons'; // import two icons
</script>
<template>
<svg aria-hidden="true">
<use :href="dogIcon" />
</svg>
<svg aria-hidden="true">
<use :href="catIcon" />
</svg>
</template>
```
You can create a very simple component **BaseIcon.vue**:
```vue
<script setup lang="ts">
defineProps<{ src: string }>()
</script>
<template>
<svg aria-hidden="true">
<use :href="src" />
</svg>
</template>
```
```vue
<script setup lang="ts">
import { dogIcon, catIcon } from 'svg:symbols@icons';
import BaseIcon from '@/components/BaseIcon.vue';
</script>
<template>
<BaseIcon :src="dogIcon" />
<BaseIcon :src="catIcon" />
</template>
```
<p align="center">
<a href="#vite-plugin-svg-symbols"><img align="center" src="https://werty1001.github.io/sep.svg" alt=""></a>
</p>
### Svelte
Plain import:
```svelte
<script lang="ts">
import dogIcon from '@icons/dog.svg?symbol';
</script>
<main>
<svg aria-hidden="true">
<use href={dogIcon} />
</svg>
</main>
```
Mass import:
```svelte
<script lang="ts">
import { dogIcon, catIcon } from 'svg:symbols@icons'; // import two icons
</script>
<main>
<svg aria-hidden="true">
<use href={dogIcon} />
</svg>
<svg aria-hidden="true">
<use href={catIcon} />
</svg>
</main>
```
You can create a very simple component **BaseIcon.svelte**:
```svelte
<script lang="ts">
export let src = '';
</script>
<svg aria-hidden="true">
<use href={src} />
</svg>
```
```svelte
<script lang="ts">
import { dogIcon, catIcon } from 'svg:symbols@icons';
import BaseIcon from './BaseIcon.svelte';
</script>
<main>
<BaseIcon src={dogIcon} />
<BaseIcon src={catIcon} />
</main>
```
<p align="center">
<a href="#vite-plugin-svg-symbols"><img align="center" src="https://werty1001.github.io/sep.svg" alt=""></a>
</p>
### React & Solid & Preact
Plain import:
```tsx
import dogIcon from '@icons/dog.svg?symbol'
function App() {
return (
<>
<svg aria-hidden="true">
<use href={dogIcon} />
</svg>
</>
)
}
export default App
```
Mass import:
```tsx
import { dogIcon, catIcon } from 'svg:symbols@icons' // import two icons
function App() {
return (
<>
<svg aria-hidden="true">
<use href={dogIcon} />
</svg>
<svg aria-hidden="true">
<use href={catIcon} />
</svg>
</>
)
}
export default App
```
You can create a very simple component **BaseIcon.tsx**:
```tsx
function BaseIcon({ src = '' }) {
return (
<svg aria-hidden="true">
<use href={src} />
</svg>
)
}
export default BaseIcon
```
```tsx
import { dogIcon, catIcon } from 'svg:symbols@icons' // import two icons
import BaseIcon from './BaseIcon'
function App() {
return (
<>
<BaseIcon src={dogIcon} />
<BaseIcon src={catIcon} />
</>
)
}
export default App
```
<p align="center">
<a href="#vite-plugin-svg-symbols"><img align="center" src="https://werty1001.github.io/sep.svg" alt=""></a>
</p>
## Options
* Inject: [shouldInjectToHtml](#shouldinjecttohtml) | [injectAttrs](#injectattrs)
* Transform: [Icon](#transformicon) | [IconId](#transformiconid) | [Sprite](#transformsprite) | [Symbol](#transformsymbol) | [ImportName](#transformimportname) | [ImportComment](#transformimportcomment)
```ts
type SymbolPayload = {
symbolId: string
symbolBody: string
symbolViewBox: string
svgBody: string
svgPath: string
svgAttrs: Record<string, string>
}
type SvgSymbolsPluginOptions = {
fileName?: string
shouldInjectToHtml?: boolean
injectAttrs?: Record<string, string | boolean | undefined>
transformIcon?: (iconCode: string, iconPath: string) => string | Promise<string>
transformIconId?: (iconId: string, iconPath: string) => string | Promise<string>
transformSprite?: (body: string) => string | Promise<string>
transformSymbol?: (symbolCode: string, payload: SymbolPayload) => string | Promise<string>
transformImportName?: (name: string, iconPath: string) => string | Promise<string>
transformImportComment?: (comment: string, iconPath: string) => string | Promise<string>
}
```
#### fileName
```js
{
fileName: 'my-sprite-name-[hash]', // default 'sprite-[hash]'
}
```
#### shouldInheritAttrs
```js
{
shouldInheritAttrs: [], // List of SVG attributes to inherit in <symbol>
}
// default: [
// 'fill',
// 'stroke',
// 'stroke-dasharray',
// 'stroke-dashoffset',
// 'stroke-linecap',
// 'stroke-linejoin',
// 'stroke-miterlimit',
// 'stroke-opacity',
// 'stroke-width',
// ];
```
```html
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="8" cy="8" r="8"></circle>
</svg>
<!-- Become -->
<symbol viewBox="0 0 16 16" id="some-id">
<g fill="none" stroke="currentColor" stroke-width="2">
<circle cx="8" cy="8" r="8"></circle>
</g>
</symbol>
```
#### shouldInjectToHtml
```js
{
shouldInjectToHtml: true, // When set to true, injects the sprite into index.html
}
```
#### injectAttrs
```js
{
injectAttrs: { ... }, // Defines sprite attributes when injected into index.html
}
// default:
// {
// 'aria-hidden': 'true',
// style: 'position:absolute;top:0;left:0;width:1px;height:1px;visibility:hidden;opacity:0;',
// }
```
#### transformIcon
```js
// Callback function to transform an icon body, for example, to optimize it with svgo:
const { optimize } = require('svgo');
const svgoOptions = {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeComments: true,
},
},
},
],
};
```
```js
{
transformIcon: (iconCode, iconPath) => {
const optimizationResult = optimize(iconCode, svgoOptions);
return optimizationResult.data;
},
}
```
#### transformIconId
```js
// Callback function to transform an icon id, for example, when you have:
// src/icons/cat.svg
{
transformIconId: (iconId, iconPath) => {
console.log(iconId); // 'icons-cat'
console.log(iconPath); // '/Users/werty1001/Desktop/app/src/icons/cat.svg'
return iconId;
},
}
```
#### transformSprite
```js
// Callback function to transform the sprite body, for example, to prepend <defs>:
{
transformSprite: (body) => {
return '<defs>...</defs>'.concat(body);
},
}
```
#### transformSymbol
```js
// Callback function to transform the sprite symbol, for example, when you have:
// src/icons/cat.svg
{
transformSymbol: (symbolCode, payload) => {
console.log(symbolCode); // '<symbol ...'
console.log(payload);
// {
// symbolId: 'icons-cat',
// symbolBody: '<path ...',
// symbolViewBox: '0 0 24 24',
// svgBody: '<path ...',
// svgPath: '/Users/werty1001/Desktop/app/src/icons/cat.svg',
// svgAttrs: { viewBox: '0 0 24 24', ... }
// }
return symbolCode;
},
}
```
#### transformImportName
```js
// Callback function to transform import name, for example, when you have:
// src/icons/cat.svg
{
transformImportName: (name, iconPath) => {
console.log(name); // 'catIcon'
console.log(iconPath); // '/Users/werty1001/Desktop/app/src/icons/cat.svg'
return name;
},
}
// import { catIcon } from 'svg:symbols@icons';
```
#### transformImportComment
```js
// Callback function to transform import comment,
// for example, when you have lucide package:
import { basename } from 'path';
const lucideComment = `/**
* Lucide
* @see https://lucide.dev/icons/[name]
*/`;
```
```js
{
// Replace the default comment to enable IDEs to show
// tooltips with a link to the icon at https://lucide.dev
transformImportComment: (comment, iconPath) => {
if (iconPath.includes('lucide-static')) {
return lucideComment.replace('[name]', basename(iconPath, '.svg'));
}
return comment;
},
}
```
<p align="center">
<a href="#vite-plugin-svg-symbols"><img align="center" src="https://werty1001.github.io/sep.svg" alt=""></a>
</p>