Skip to content

Named icons

Named icons must be implemented on your side using import.meta.glob() or, given your circumstances, another method.

Named icons are not provided out of the box because:

  1. Variable interpolation is not supported in glob imports.
  2. Glob imports can get highly project-specific. vite-awesome-svg-loader avoids such specificity.

For more detail on glob import, see: https://vitejs.dev/guide/features#glob-import

The demo below presents an example named icons implementation.

Rendered demo
Demo's source code
src
assets
icons
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M21 5v6.59l-3-3.01l-4 4.01l-4-4l-4 4l-3-3.01V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2m-3 6.42l3 3.01V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-6.58l3 2.99l4-4l4 4"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6 2v6l4 4l-4 4v6h12v-6l-4-4l4-4V2z"/></svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.28003 22C8.00316 22 9.40003 20.6031 9.40003 18.88C9.40003 17.1569 8.00316 15.76 6.28003 15.76C4.55691 15.76 3.16003 17.1569 3.16003 18.88C3.16003 20.6031 4.55691 22 6.28003 22Z" stroke="red" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.84 16.8001V4.60009C20.84 2.00009 19.21 1.64009 17.56 2.09009L11.32 3.79009C10.18 4.10009 9.40002 5.00009 9.40002 6.30009V8.47009V9.93009V18.8701" stroke="red" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.72 19.9199C19.4431 19.9199 20.84 18.5231 20.84 16.7999C20.84 15.0768 19.4431 13.6799 17.72 13.6799C15.9968 13.6799 14.6 15.0768 14.6 16.7999C14.6 18.5231 15.9968 19.9199 17.72 19.9199Z" stroke="red" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.40002 9.5199L20.84 6.3999" stroke="red" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.73 3.51001L15.49 7.03001C15.73 7.52002 16.37 7.99001 16.91 8.08001L20.1 8.61001C22.14 8.95001 22.62 10.43 21.15 11.89L18.67 14.37C18.25 14.79 18.02 15.6 18.15 16.18L18.86 19.25C19.42 21.68 18.13 22.62 15.98 21.35L12.99 19.58C12.45 19.26 11.56 19.26 11.01 19.58L8.01997 21.35C5.87997 22.62 4.57997 21.67 5.13997 19.25L5.84997 16.18C5.97997 15.6 5.74997 14.79 5.32997 14.37L2.84997 11.89C1.38997 10.43 1.85997 8.95001 3.89997 8.61001L7.08997 8.08001C7.61997 7.99001 8.25997 7.52002 8.49997 7.03001L10.26 3.51001C11.22 1.60001 12.78 1.60001 13.73 3.51001Z" stroke="green" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.53 20.4201H6.21C3.05 20.4201 2 18.3201 2 16.2101V7.79008C2 4.63008 3.05 3.58008 6.21 3.58008H12.53C15.69 3.58008 16.74 4.63008 16.74 7.79008V16.2101C16.74 19.3701 15.68 20.4201 12.53 20.4201Z" stroke="#7272ff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.52 17.0999L16.74 15.1499V8.83989L19.52 6.88989C20.88 5.93989 22 6.51989 22 8.18989V15.8099C22 17.4799 20.88 18.0599 19.52 17.0999Z" stroke="#7272ff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.5 11C12.3284 11 13 10.3284 13 9.5C13 8.67157 12.3284 8 11.5 8C10.6716 8 10 8.67157 10 9.5C10 10.3284 10.6716 11 11.5 11Z" stroke="#7272ff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
import loadingIcon from "@/assets/icons/hourglass.svg";
import errorIcon from "@/assets/icons/broken-image.svg";
import { SvgIcon, WebComponentDefinitionOptions } from "vite-awesome-svg-loader/web-components-integration";
import { CustomElement } from "typed-custom-elements";
// See: https://vitejs.dev/guide/features#glob-import
const rawIcons: any = import.meta.glob("/src/assets/icons/*.svg", {
// Put URL here or setup your imports via vite-awesome-svg-loader configuration
query: "?preserve-line-width&set-current-color",
});
// Transform keys from paths to file-based icon names
const icons: any = {};
for (const path in rawIcons) {
let name = path.split("/").pop() || "";
name = name.substring(0, name.lastIndexOf("."));
icons[name] = rawIcons[path];
}
// Extend SvgIcon.
//
// Avoid nesting web components because each web component is an actual DOM element. Nesting defeats
// the whole purpose of using SVG symbols to reduce the amount of DOM nodes and improve rendering performance.
//
// If you want composition, consider creating your own wrapper or using a specialized library/framework for creating
// components.
export class NamedIcon extends SvgIcon implements CustomElement {
// Create public "name" property
static props = ["name"];
/** Icon name */
declare name?: string; // Declare "name" property, so it will be typed
// We do not override "src" property because:
//
// 1. Parent's behavior depends on it.
// 2. Inheritance pattern prevents us from removing a property. What if child removes it, but grandchild decides to
// return it back? This is too cumbersome to manage.
//
// Instead, we gracefully handle outer "src" changes by clearing "name" property.
/** An internal locking mechanism that prevents infinite recursion when handling source code changes */
private srcChangedBy: "src" | "name" | undefined;
// React to "name" and "src" changes
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
super.attributeChangedCallback(name, oldValue, newValue); // Parent method must be called
switch (name) {
case "name":
this.onNameChange(oldValue, newValue);
break;
case "src":
this.onSrcChange(oldValue, newValue);
break;
}
}
// Load icon whenever name is changed by the user
private async onNameChange(oldName: string | null, name: string | null) {
// Do not react, if current change has been caused by "src" change
if (this.srcChangedBy === "src") {
return;
}
// Prevent infinite recursion when setting src
const setSrc = (code: string) => {
this.srcChangedBy = "name";
this.src = code;
this.srcChangedBy = undefined;
};
// Don't render anything, if there's no icon name
if (!name) {
setSrc("");
return;
}
// Set loading icon if icon has been hidden due to missing source code
if (!oldName && !this.src) {
setSrc(loadingIcon);
}
let code: string;
try {
code = (await icons[name]()).default; // Fetch SVG source code
} catch (e) {
console.error(e);
code = errorIcon; // Provide a fallback for when icon could not be loaded
}
// Verify that name hasn't been changed. If it didn't, set its source code. Otherwise other setName()
// call will handle the changes.
if (name === this.name) {
setSrc(code);
}
}
// Clear icon name whenever "src" is changed by the user
private onSrcChange(src: string | null, oldSrc: string | null) {
// Do not react, if:
// - Component just mounted (let onNameChange() take over).
// - Current change has been caused by "name" change.
if (src === oldSrc || this.srcChangedBy === "name") {
return;
}
// Clear name when source code has been changed outside of onNameChange()
this.srcChangedBy = "src";
this.name = undefined;
this.srcChangedBy = undefined;
}
// Change default tag to "named-icon"
static define(options: WebComponentDefinitionOptions = {}) {
super.define({ ...options, tag: options.tag || "named-icon" });
}
}
// Add DOM type definitions. These definitions will be picked up automatically when NamedIcon will be imported at least
// once.
declare global {
interface HTMLElementTagNameMap {
"named-icon": NamedIcon;
}
}
import { NamedIcon } from "@/NamedIcon";
export function main() {
NamedIcon.define();
document.getElementById("app")!.innerHTML += `
<div class="images">
<named-icon
name="music"
color="red"
></named-icon>
<named-icon
name="star"
color="forestgreen"
></named-icon>
<named-icon
name="video"
color="cornflowerblue"
></named-icon>
</div>
`;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1"
/>
</head>
<body>
<div id="app"></div>
<script type="module">
import { main } from "./src/main.ts";
main();
</script>
</body>
</html>
{
"name": "web-components-named-icons",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "npm run build -- --watch",
"build": "npm run type-check && npm run build-only",
"build-only": "vite build --config vite.config.lib.ts",
"type-check": "tsc --noEmit"
},
"dependencies": {
"vite-awesome-svg-loader": "*",
"vite-file-tree-builder": "*",
"vite-astro-entry-generator": "*"
}
}
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": [
// Add types for query imports (for example, "@/some/file.svg?src").
// Unfortunately, it's impossible to fully type it. Use plugin configuration instead.
"vite-awesome-svg-loader",
// Add types for default custom tags
"vite-awesome-svg-loader/web-components-integration/dom",
"vite/client", // Vite types
"vite-file-tree-builder" // Internal types
]
},
"include": ["src"]
}
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
// Import vite-awesome-svg-loader
import { viteAwesomeSvgLoader } from "vite-awesome-svg-loader";
export default defineConfig({
plugins: [viteAwesomeSvgLoader({ urlImportsInLibraryMode: "emit-files" })],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
Please open file to view its content