Installation
What gets installed, why exactly that, and where it goes — dependencies or devDependencies. Plus icons and optional integrations.
Getting started gives you one working path with no explanations. This page explains: why the dependency list looks the way it does, what of it reaches the bundle and what only ever runs at build time.
Requirements
| What | Version | Why it is strict |
|---|---|---|
| Node | ≥ 22 | Declared in engines, the package is ESM-only |
| ES modules | required | The package is ESM-only ("type": "module", no require condition); there is no CommonJS build and none is planned |
| Vue | ^3.5 | Peer dependency |
@floating-ui/dom | ^1.8 | Required runtime peer dependency |
@feugene/granum | ^1.0 | The CSS pipeline; build time only, never in the bundle |
@feugene/granum-engine-wind | ^1.0 | The utility vocabulary it runs on |
| Vite | ^8 | granum declares it as an optional peer |
@feugene/fint-i18n is a peer too, but an optional one: without it the
components speak English through their built-in strings rather than failing.
Details are on the localization page.
The one supported way
Everything goes through @feugene/granum — the pipeline that reads the package
manifest and assembles a single sheet from it. Importing CSS files directly
and pulling in styles.css around the pipeline are not supported — they work right
up to the first upgrade and then break silently. A component from the package root
does not get through granum at all: it is a ComponentRootImportError build error,
and components come from their subpaths.
The manifest is not a file-naming convention but a declared export:
@feugene/granularity/granum.manifest.json. That is why granum never scans
node_modules and never guesses a package’s layout: it has no such channel at
all.
The exception is named by the package itself: @feugene/granularity/styles.css
is a ready-made sheet with the tokens, the base layer, the preflight and both
themes, for consumers who do not run the pipeline at all. Its parts are also
exported one by one — @feugene/granularity/styles/* (tokens.css, base.css,
preflight.css, themes/light.css, themes/dark.css) — for the same case.
Neither gives you the utility classes the component templates are drawn with.
What goes where
# in dependencies — it ends up inside the application
yarn add vue @feugene/granularity @floating-ui/dom @unocss/reset
# in devDependencies — it only ever runs at build time
yarn add -D @feugene/granum @feugene/granum-engine-wind@feugene/granularity goes into dependencies: its components are imported
from your sources and land in the bundle.
@floating-ui/dom goes into the application’s dependencies, not inside the
package. It positions every floating panel — GrSelect, GrAutocomplete,
GrTreeSelect, GrDropdown, GrDropdownMenu, GrContextMenu, GrTooltip,
GrPopover, GrColorPicker — and, through them, GrPagination, GrDataTable
and GrSidebar. The package keeps the library external and does not bundle it
into dist — otherwise an application that already uses floating-ui would end up
with a second copy.
@feugene/granum and the engine go into devDependencies: they run at
build time and not a single line of them reaches the bundle. Both have zero
dependencies: the preset-wind3 vocabulary is vendored inside the engine, so
there is no need to install unocss — it will not be in your tree at all.
@unocss/reset goes into dependencies, as in the core’s own getting
started: its sheet ends up in the application’s CSS — through a single @import
line in your CSS rather than an import from code.
The modal layer — GrModal, GrDialog, GrDrawer, GrImageViewer,
GrCommandPalette — needs no external dependencies at all. The focus trap,
inert for the background, the Esc order and focus restoration are the package’s
own primitives. @headlessui/vue was dropped in 0.15.0 and is not a peer
dependency: if it is still installed for this package alone, it can go.
Growing the config
The minimal working config is the engine, the provider and your source folder:
import { windEngine } from '@feugene/granum-engine-wind'
import { defineGranumConfig } from '@feugene/granum/vite'
export default defineGranumConfig({
engine: windEngine(),
providers: ['@feugene/granularity'],
appSources: { dirs: ['src'] },
})import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import { granum } from '@feugene/granum/vite'
import granumConfig from './granum.config.mjs'
export default defineConfig({
plugins: [Vue(), granum(granumConfig)],
})That already mixes in the tokens, the base layer and the light theme, enables
every component of the provider with its styles, and brings up the rules,
variants and safelist.
From there you narrow and refine with options:
| Option | What it does |
|---|---|
components | A list instead of 'all', or 'imports' — by what appSources imports. The one place where you pay for granularity with attention |
themes.names | Which themes reach the sheet. Without it granum takes the themes the provider declares as defaults — for the core that is light alone. Name the dark one explicitly: ['light', 'dark']. How the set is chosen is in the granum docs (ru, en) |
appSources.dirs | Where to look for the classes in your own markup |
css.layers | Cascade layers. On by default; turning them off costs predictability, not bytes |
pruneTokens.mode | 'on' drops tokens nobody uses from the sheet; 'report' only lists them in the report. 'off' by default |
js.guard | Importing a component outside components: an error (the default), a warning or silence |
There is no “where to look for classes inside node_modules” option, and none is
needed: a component’s classes arrive from the package manifest, already counted
when the package was built. In the v1 preset this was a hand-written
content.filesystem line, and a utility that only ever appeared inside a lazily
loaded island never reached the sheet.
Icons
The package brings its own icons with it. The select arrow, the clear cross,
the check mark on the chosen option, the spinner and the drag handle are compiled
into dist when the package is built. You do not need unplugin-icons or an
icon collection for them.
Your own icon arrives in one of two ways. The icon props take either a Vue
component or a class:
<script setup lang="ts">
import { ref } from 'vue'
import { GrTabs } from '@feugene/granularity/components/GrTabs'
import IconUser from '~icons/lucide/user'
const tab = ref('a')
</script>
<template>
<GrTabs v-model="tab" :tabs="[{ value: 'a', label: 'Profile', icon: IconUser }]" />
<GrTabs v-model="tab" :tabs="[{ value: 'a', label: 'Profile', icon: 'i-lucide-user' }]" />
</template>The first line passes a component, and the package does not care where it comes
from — here it comes from unplugin-icons, which the application installs and
configures together with the @iconify-json/lucide collection. The second passes
a class: the CSS for it is generated by your config, not by the package.
i-lucide-* is a utility, and your build is what makes it. Icon rules are not
part of the preset-wind3 vocabulary: without a rule the class stays a class —
the space for the icon is there, the picture is not, and the build passes in
silence.
yarn add -D @iconify-json/lucideThe application writes the rule itself — neither granum nor the engine ships one — and hands it to the engine factory:
import { windEngine } from '@feugene/granum-engine-wind'
export default defineGranumConfig({
engine: windEngine({ rules: [iconRule] }),
providers: ['@feugene/granularity'],
})A rule handed to the factory changes the application’s vocabulary fingerprint. It no longer matches the one the package manifests were computed under, and granum re-extracts every provider’s classes from its files.
It shows even with a single package: with just the core, the re-extraction takes about 0.5 s of a cold build for no difference in the result (core 1.1.1, granum 2.1); on the design system’s showcase with eight providers it is 609 ms. For this case the granum docs advise moving the rules into a provider package of their own (ru, en): rules that arrive from a provider leave the application’s fingerprint alone. The portal is built the same way — its icon rules arrive as a ninth provider.
Two CSS imports, and the order between them is load-bearing
The sheet is not injected by an integration — your application imports it by hand at the entry point.
import './reset.css'
import 'virtual:granum.css'@import '@unocss/reset/tailwind-compat.css' layer(reset);The reset goes into its own layer, and that is not decoration. granum puts
everything of its own into granum.* layers, and unlayered CSS beats ANY
@layer regardless of specificity: outside a layer, button { color: inherit }
from the reset would override the component’s own utilities, and the button would
arrive in the page’s text colour.
The layer must be declared before granum’s — a layer’s place in the cascade
is fixed by its first appearance — which is why reset.css is imported first.
Getting the order wrong produces neither an error nor a warning: the build
passes, only the picture breaks.
Optional integrations
@feugene/unplugin-granularity— a resolver forunplugin-vue-components: auto-import of components and directives in templates. It inserts static imports into the SFC, so tree-shaking stays as tight as it was.createGranularityfrom@feugene/granularity/vue— a runtime adapter for directives used inrender/JSX, for a sharedprovideand forglobalProperties. It imports no components itself: what you pass is what gets registered.
Both complement the main way of installing rather than replacing it. Setting up either one is covered in configuration.