Installation

  • Get started
  • 9 min
  • 8 snippets

What gets installed, why exactly that, and where it goes — dependencies or devDependencies. Plus icons and optional integrations.

Reviewed 2026-10-16

On this page

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

WhatVersionWhy it is strict
Node≥ 22Declared in engines, the package is ESM-only
ES modulesrequiredThe package is ESM-only ("type": "module", no require condition); there is no CommonJS build and none is planned
Vue^3.5Peer dependency
@floating-ui/dom^1.8Required runtime peer dependency
@feugene/granum^1.0The CSS pipeline; build time only, never in the bundle
@feugene/granum-engine-wind^1.0The utility vocabulary it runs on
Vite^8granum 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

bash
# 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:

granum.config.mjs
import { windEngine } from '@feugene/granum-engine-wind'
import { defineGranumConfig } from '@feugene/granum/vite'

export default defineGranumConfig({
  engine: windEngine(),
  providers: ['@feugene/granularity'],
  appSources: { dirs: ['src'] },
})
vite.config.ts
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:

OptionWhat it does
componentsA list instead of 'all', or 'imports' — by what appSources imports. The one place where you pay for granularity with attention
themes.namesWhich 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.dirsWhere to look for the classes in your own markup
css.layersCascade 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.guardImporting 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:

vue
<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.

bash
yarn add -D @iconify-json/lucide

The application writes the rule itself — neither granum nor the engine ships one — and hands it to the engine factory:

granum.config.mjs
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.

src/main.ts
import './reset.css'
import 'virtual:granum.css'
src/reset.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 for unplugin-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.
  • createGranularity from @feugene/granularity/vue — a runtime adapter for directives used in render/JSX, for a shared provide and for globalProperties. 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.