# Installation

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

- Page: https://granularity.tech/docs/installation
- Last reviewed: 2026-10-16

[Getting started](/docs/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](/docs/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.

:::callout{type="info"}
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:

```js title="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'] },
})
```

```ts title="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:

| 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](https://github.com/efureev/granum/blob/main/docs/ru/themes-and-tokens.md), [en](https://github.com/efureev/granum/blob/main/docs/en/themes-and-tokens.md)) |
| `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:

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

:::callout{type="warning"}
`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:

```js title="granum.config.mjs"
import { windEngine } from '@feugene/granum-engine-wind'

export default defineGranumConfig({
  engine: windEngine({ rules: [iconRule] }),
  providers: ['@feugene/granularity'],
})
```

:::callout{type="info"}
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](https://github.com/efureev/granum/blob/main/docs/ru/usage-in-apps.md),
[en](https://github.com/efureev/granum/blob/main/docs/en/usage-in-apps.md)):
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.

```ts title="src/main.ts"
import './reset.css'
import 'virtual:granum.css'
```

```css title="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`](https://www.npmjs.com/package/@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](/docs/configuration).
