Getting started

  • Get started
  • 5 min
  • 7 snippets

Five steps from an empty directory to a coloured button on screen. Everything deeper lives on the neighbouring pages.

Reviewed 2026-10-16

On this page

The shortest useful page on this site. No options, no caveats, no “but what if you are on Nuxt” — one path that gets you to a working component. The branches start on installation and in configuration.

What you need

  1. Node 22 or newer. That is what the package declares in engines: Yarn 1 refuses to install it on an older Node, npm and pnpm only warn. The package itself is ESM-only — "type": "module" and not a single require condition in exports.
  2. An application built on ES modules. The package has no CommonJS build and is loaded with import, as Vite does. "type": "module" in the application’s package.json is recommended but never checked: the granum config here is .mjs anyway.
  3. Vue 3.5 or newer and a Vite 8 build — that is the peer @feugene/granum declares. Other bundlers are not forbidden, but they are not verified either.

Install

npm i @feugene/granularity vue @floating-ui/dom @unocss/reset
pnpm add @feugene/granularity vue @floating-ui/dom @unocss/reset
yarn add @feugene/granularity vue @floating-ui/dom @unocss/reset
bun add @feugene/granularity vue @floating-ui/dom @unocss/reset

And the part that only runs at build time and never reaches the bundle:

npm i -D vite @vitejs/plugin-vue @feugene/granum @feugene/granum-engine-wind typescript vue-tsc
pnpm add -D vite @vitejs/plugin-vue @feugene/granum @feugene/granum-engine-wind typescript vue-tsc
yarn add -D vite @vitejs/plugin-vue @feugene/granum @feugene/granum-engine-wind typescript vue-tsc
bun add -d vite @vitejs/plugin-vue @feugene/granum @feugene/granum-engine-wind typescript vue-tsc

@floating-ui/dom is a required peer dependency, not a recommendation. It positions every floating panel — GrSelect, GrAutocomplete, GrTreeSelect, GrDropdown, GrDropdownMenu, GrContextMenu, GrTooltip, GrPopover, GrColorPicker — and everything that renders one, such as GrPagination, GrDataTable and GrSidebar. Without it the build stops at the first such import: Vite cannot resolve @floating-ui/dom. The modal layer, by contrast, needs nothing: the focus trap, inert and the Esc order are the package’s own primitives.

Configuration

The design system’s CSS is not imported as files — granum assembles it. It reads the package’s contents from the manifest the package declares as an export and never walks node_modules: it opens the package’s files only when the manifest cannot be trusted, and only the files the manifest lists. There is exactly one source of truth:

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

export default defineGranumConfig({
  engine: windEngine(),
  providers: ['@feugene/granularity'],
  components: [{ provider: '@feugene/granularity', names: ['GrButton'] }],
  themes: { names: ['light', 'dark'] },
  appSources: { dirs: ['src'] },
})

components can be left out entirely — then every component of the provider lands in the CSS. Listing them narrows the sheet down to the set you need, and this is the one place where you pay for granularity with attention.

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)] })

The config is read in one place, not two. The v1 preset had to be handed the same object both in content and in presets: let them drift apart and components arrived unstyled, with the build saying nothing. That fork is gone.

Two imports, and the order between them is load-bearing

src/main.ts
import './reset.css'
import 'virtual:granum.css'

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
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.

There is one virtual CSS module now — virtual:granum.css. The tokens, the base layer, the themes, the component styles and the utilities their templates are drawn with all arrive in a single sheet: forgetting half the foundation and getting colours without geometry is no longer possible.

The first component

src/App.vue
<script setup lang="ts">
import { GrButton } from '@feugene/granularity/components/GrButton'
</script>

<template>
  <GrButton tone="primary">Save</GrButton>
</template>

Importing from a subpath rather than from the package root is granularity itself. It pulls one component and its graph, not a barrel. granum rejects a component from the root as a build error, ComponentRootImportError.

Run vite. The button should be coloured, with rounded corners and a focus ring on Tab. If it is grey and bare, the sheet is not imported at all. If the background is there but the text is in the page’s colour and the padding is gone, the reset is imported after the sheet. Both cases are worked through on the troubleshooting page.

Next

  • Installation — why the dependency list looks like this, icons, optional integrations.
  • Configuration — defaults, locale, overrides at the application level.
  • Theming — rebranding without touching components.
  • Component catalogue — what there is at all.