# 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-20

[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` | `^3.8` | The CSS pipeline; build time only, never in the bundle |
| `@feugene/granum-engine-wind` | `^3.8` | 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 `slate` theme in both modes and the base layer, for
consumers who do not run the pipeline at all. Its parts are also exported one by
one — `@feugene/granularity/themes/slate.css` (primitives, modes, component tokens)
and `@feugene/granularity/styles/*` (`base.css`, `preflight.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.

**`@feugene/granularity-tokens` needs no installing:** it arrives as the core's
single dependency. It is the token dictionary and the `slate` theme the build takes
its values from; it has no dependencies of its own, and the core does not import
its code at run time. It is declared explicitly when you assemble a mode or a
theme of your own — see [theming](/docs/theming).

**`@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 `slate` theme in its default mode, 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` | The build themes and their modes: `[{ theme: 'slate', modes: ['light', 'dark'] }]`. The default theme lives in the shared sheet, additional ones in sheets of their own. Without it — `slate` in a single default mode, `light`. Your own modes go into the entry's `modes.define`, pointwise values into `modes.tokenOverrides`. More 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).
