Typed SVG sprites and source icons for OmnicaJS projects. The package exposes the same catalog as full sprites, group sprites, raw SVG files, and consumer-built subsets.
yarn add @omnicajs/iconsMonochrome icons are identified by variant/group/name:
filled/actions/add
outlined/actions/add-circle
outlined is a smaller independent catalog. There is no automatic fallback to filled: TypeScript rejects a missing outlined name, and JavaScript receives an explicit runtime error.
The move from legacy groups and -outlined filenames is intentionally breaking; no runtime aliases are installed. The complete old-path to variant/group/name map is published as @omnicajs/icons/migrations/groups-v1.json for migrations and search tooling.
Both full and group sprites use the same symbol fragment, group/name. Only the sprite file changes:
<filled sprite URL>#actions/add
<filled actions sprite URL>#actions/add
The root and /all entrypoints include the complete filled and outlined registries:
import { iconUrl, spriteUrl } from '@omnicajs/icons'
const add = iconUrl('filled', 'actions', 'add')
const outlinedAdd = iconUrl('outlined', 'actions', 'add-circle')
const filledSprite = spriteUrl('filled')If an application only needs one variant, use its narrower full-sprite entrypoint:
import { iconNames, iconUrl, spriteUrl } from '@omnicajs/icons/filled'
const add = iconUrl('actions', 'add')An entrypoint for one group keeps both runtime code and declarations local to that group:
import { iconNames, iconUrl, spriteUrl } from '@omnicajs/icons/filled/actions'
const add = iconUrl('add')Use /groups when the delivery mode must be selected dynamically while retaining exact variant types:
import { iconUrl, spriteUrl } from '@omnicajs/icons/groups'
const add = iconUrl('filled', 'actions', 'add')
const actionsSprite = spriteUrl('filled', 'actions')Render any returned URL through an external SVG use:
<svg width="24" height="24" aria-hidden="true" style="color: #005eeb">
<use href="<resolved icon URL>"></use>
</svg>Monochrome sprites inherit currentColor; their source viewBox is preserved.
Full-color collections have independent entrypoints and preserve their source colors:
import { iconUrl as flagUrl } from '@omnicajs/icons/flags'
import { iconUrl as logoUrl } from '@omnicajs/icons/logos'
const armenia = flagUrl('armenia')
const telegram = logoUrl('telegram')Use raw assets for one or two icons or for a consumer-owned SVG pipeline:
import addUrl from '@omnicajs/icons/assets/icons/filled/actions/add.svg?url'
import armeniaUrl from '@omnicajs/icons/assets/flags/armenia.svg?url'Ready-made files are also exported under @omnicajs/icons/sprites/*, and the typed machine-readable catalog is available as @omnicajs/icons/manifest. The raw JSON remains available as @omnicajs/icons/manifest.json.
The manifest records raw and gzip byte sizes for every full and group sprite. This keeps size reporting aligned with the installed package instead of a README snapshot:
import manifest from '@omnicajs/icons/manifest'
const filledSize = manifest.variants.filled.size
const actionsSize = manifest.variants.filled.groups.actions.size
// { bytes: number, gzipBytes: number }Every manifest icon also has a keywords array with designer-provided search associations. Keywords are search metadata only: the canonical name, symbol ID, and runtime URL remain unchanged.
const brainKeywords = manifest.variants.filled.groups.ai.icons['brain-circuit'].keywords
// ['ai/brain-ai']The opt-in Vue adapter complements the existing full, group, and configured subset APIs. It supports Vue 3 and transforms only unqualified filled and outlined package assets.
Add the adapter before the regular Vue plugin:
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import { omnicaIconComponents } from '@omnicajs/icons/vite'
export default defineConfig({
plugins: [
omnicaIconComponents(),
vue(),
],
})Add the icon plugin before VueLoaderPlugin. The adapter injects its package-scoped SVG rule and reuses the application's vue-loader:
const { VueLoaderPlugin } = require('vue-loader')
const { OmnicaIconComponentsPlugin } = require('@omnicajs/icons/webpack')
module.exports = {
output: {
publicPath: '/',
},
module: {
rules: [
{ test: /\.vue$/, loader: 'vue-loader' },
{ test: /\.svg$/, type: 'asset/resource' },
],
},
plugins: [
new OmnicaIconComponentsPlugin(),
new VueLoaderPlugin(),
],
}Rspack uses the same contract and its own typed entrypoint:
const { VueLoaderPlugin } = require('vue-loader')
const { OmnicaIconComponentsPlugin } = require('@omnicajs/icons/rspack')
module.exports = {
output: {
publicPath: '/',
},
module: {
rules: [
{ test: /\.vue$/, loader: 'vue-loader' },
{ test: /\.svg$/, type: 'asset/resource' },
],
},
plugins: [
new OmnicaIconComponentsPlugin(),
new VueLoaderPlugin(),
],
}Both configurations require the normal Vue 3 SFC toolchain (vue, vue-loader, and @vue/compiler-sfc) in the application. The package does not declare a root Vue peer because the adapter is opt-in and the sprite/URL APIs remain compatible with non-Vue and Vue 2 consumers. If an application already has another SVG loader, constrain that rule with a query or exclude @omnicajs/icons/assets/icons so one unqualified import is not processed by two component loaders.
An unqualified monochrome SVG import then resolves to a Vue component:
<script setup lang="ts">
import IconClearCircle from '@omnicajs/icons/assets/icons/filled/actions/clear-circle.svg'
</script>
<template>
<IconClearCircle width="24" height="24" aria-hidden="true" />
</template>Load the opt-in declarations from a project declaration file included by the consumer tsconfig, for example src/omnica-icons.d.ts:
import '@omnicajs/icons/vue'The /vue entrypoint exports OmnicaIconProps as Vue's SVGAttributes plus typed data-* attributes, and OmnicaIconComponent as DefineComponent<OmnicaIconProps>. Every transformed asset uses that component type, so TypeScript checks standard SVG presentation, sizing, accessibility, data, and event attributes instead of exposing an unqualified generic Vue component.
Production builds emit one content-hashed sprite containing the imported filled and outlined symbols. Generated components preserve the source viewBox, forward attributes to their root svg, and inherit currentColor. The symbol fragment includes the variant, for example #filled/actions/clear-circle, so filled and outlined names cannot collide. Vite honours its configured root-relative base; Webpack and Rspack use their root-relative output.publicPath.
For SSR, run the client build first. It emits .omnica/omnica-icons-imported.json; the Vite, Webpack, and Rspack SSR adapters read that manifest and reference the exact client sprite instead of hashing the server graph independently. Server-rendered icons must be a subset of the client graph, otherwise the SSR build fails with the missing symbol names instead of producing a URL that would return 404.
The default client output is <root>/dist for Vite and <context>/dist for Webpack/Rspack. When the client uses another output directory, pass it to both configurations:
omnicaIconComponents({ clientOutputDirectory: 'dist/client' })new OmnicaIconComponentsPlugin({ clientOutputDirectory: 'dist/client' })All production builds require a root-relative public path such as /app/: configure it through Vite base or Webpack/Rspack output.publicPath. Absolute HTTP(S), protocol-relative, automatic, empty, and relative paths are rejected. The generated components use external SVG <use> references, which browsers restrict to the page origin, so CDN sprite URLs are not supported. SSR builds reuse the public sprite URL recorded by the client manifest even when the server bundler has another root-relative public path.
Vite development serves content-addressed one-symbol sprites so HMR can add or remove imports without stale combined-sprite state. Vite watch builds and Webpack/Rspack watch compilations recalculate the production sprite from the current module graph.
Imports with ?url keep the bundler's standard string URL behavior. Static asset URLs in Vue templates, CSS url() references, and JavaScript new URL(..., import.meta.url) dependencies also remain in the normal asset pipeline; only module imports become components. Flags and logos are not transformed by this adapter and remain available through their existing URL and sprite APIs.
The package can generate one sprite per selected variant. Create omnica-icons.config.mjs:
import { defineConfig } from '@omnicajs/icons/build'
export default defineConfig({
outputDirectory: 'src/generated/omnica-icons',
include: {
filled: {
actions: ['add', 'remove'],
alerts: ['warning'],
},
outlined: {
actions: ['add-circle'],
},
},
})Generate files before the application build:
omnica-icons build --config omnica-icons.config.mjsUse omnica-icons watch during development. The output contains adjacent filled.svg/filled.ts and outlined.svg/outlined.ts files. Generated TypeScript uses a static new URL('./filled.svg', import.meta.url), so Vite and Webpack process it as a normal asset and add their production content hash.
Generated files may be committed or produced by a predev/prebuild script. The package never writes to a consumer workspace from postinstall.
The Node-only /build entrypoint also exports pure validateSelection, resolveSelection, createSprite, and createRuntimeModule primitives. I/O is explicit in loadManifest, loadIconSymbols, buildIconSet, and writeGeneratedFiles.
The Vite adapter emits hashed subset sprites and provides virtual:omnicajs-icons:
import { defineConfig } from 'vite'
import { omnicaIcons } from '@omnicajs/icons/vite'
export default defineConfig({
plugins: [
omnicaIcons({
declarationFile: 'src/omnica-icons.d.ts',
include: {
filled: { actions: ['add', 'remove'] },
outlined: { actions: ['add-circle'] },
},
}),
],
})import { iconUrl } from 'virtual:omnicajs-icons'
iconUrl('filled', 'actions', 'add')Include declarationFile in the consumer tsconfig. It describes the exact selected subset, including the incomplete outlined catalog. In development the plugin serves sprites with a content-derived query key and Cache-Control: no-cache; in production Vite owns the hashed asset filename.
The Webpack adapter uses the same selection and virtual module:
const { OmnicaIconsPlugin } = require('@omnicajs/icons/webpack')
module.exports = {
plugins: [
new OmnicaIconsPlugin({
declarationFile: 'src/omnica-icons.d.ts',
filename: 'assets/omnica-[variant].[contenthash:8].svg',
include: {
filled: { actions: ['add', 'remove'] },
outlined: { actions: ['add-circle'] },
},
}),
],
}Application code imports virtual:omnicajs-icons exactly as in the Vite example. The plugin emits separate filled and outlined assets through Webpack and builds their runtime URLs from __webpack_public_path__.
Files published by this package intentionally have stable names. A consumer build should own cache invalidation:
node_modules/@omnicajs/icons/dist/sprites/filled.svg
→ /assets/filled.616dce3a.svg#actions/add
Vite, Webpack, and the first-party adapters all treat sprites as assets and produce content-hashed production filenames. When the SVG changes, its URL changes, so browser and CDN caches cannot retain an old icon set.
The fragment is not part of the HTTP request. If an environment copies a stable sprite directly, put a version or content key before #:
/icons/filled.svg?v=<build-id>#actions/add
Do not add a random timestamp in production. Prefer content hashes, immutable caching for hashed files, and a short or revalidated cache policy for stable filenames.
The repository contains CLI, Vite, and Webpack consumer fixtures. Run make test; compile/type checks use the Node environment and external <use> rendering runs in a pinned Playwright container across Chromium, Firefox, and WebKit. The bundler fixtures verify hashed subset output, the full sprite, a group sprite, and exact TypeScript declarations.
MIT