# Storybook

> The library has no Storybook of its own and will not have one. How to plug the components into yours — config, theme, overlays and the main trap.

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

Granularity has no Storybook of its own, and none is planned. Behind the question
"do you have a Storybook?" there are almost always two others: **does the company
get a single showcase of components** and **does the library fit into its visual
testing processes**. The answer to both is yes, and this page is about how.

## Why not one of our own

The portal already does what a Storybook is set up for: live demos on every
component page, an API table straight from the code, a playground with editable
props and accessibility checks. A second such tool alongside
would mean a second source of truth and the duplication of hundreds of demos —
with a single maintainer that is a straight road to divergence, and pairs like
that diverge silently.

The same goes for Chromatic and Histoire: nothing of our own is built.

Your own Storybook still makes sense — but for a different reason. **Your**
components live in it, assembled out of ours, and it is they that need a
showcase.

## What to plug in

Granularity differs from any other Vue library in one respect only: its CSS is
not imported as a file but assembled by granum from the package manifest
([installation](/docs/installation)). So the whole setup is about making the
granum plugin run inside Storybook.

### The build

Storybook on Vite (`@storybook/vue3-vite`) reads your `vite.config.ts`. If the
granum plugin is already there, there is nothing to add to `.storybook/main.ts`:

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

If Storybook has a config of its own, the plugin is added through `viteFinal`:

```ts title=".storybook/main.ts"
import { granum } from '@feugene/granum/vite'
import granumConfig from '../granum.config.mjs'

export default {
  framework: '@storybook/vue3-vite',
  stories: ['../src/**/*.stories.@(ts|tsx)'],
  viteFinal: async config => ({
    ...config,
    plugins: [...(config.plugins ?? []), granum(granumConfig)],
  }),
}
```

### The styles

The same two imports as in the entry point of the application — in `preview.ts`,
and the order between them is just as load-bearing:

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

:::callout{type="warning"}
The reset has to sit in a layer declared before the `granum.*` layers —
otherwise it wins against the whole library, and the stories render with no
borders and colourless buttons. The details are in
[troubleshooting](/docs/troubleshooting).
:::

## The trap: the stories are not in `appSources`

The stories rendered, the components in them look right — and a utility class
written inside a story (a `grid gap-4` wrapper, the width of the stage) does
nothing.

A component's classes arrive from the package manifest and do not depend on this.
The classes of **your** markup, though, granum looks for on disk, in the
`appSources.dirs` directories. If the stories live outside `src`, they have to be
named:

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

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

The older wording of this trap — "the extractor does not look into the package's
`dist`" — does not apply to granum: a component's classes were counted when the
package was built and live in its manifest, and granum rereads the package's
files itself only when your engine's vocabulary has drifted from the package's.
None of this depends on `appSources`. Exactly half the problem is left, and that
half is about your code, not somebody else's.

:::callout{type="info"}
Listing `components` in a Storybook config is usually unnecessary: a showcase by
its nature shows everything, and the weight of its CSS bothers nobody. Narrowing
the list is a production-application technique, and it stays there.
:::

## Wrapping the stories

Subtree defaults and the target for overlays are set by `GrConfigProvider`. In
Storybook it lives in a decorator:

```ts title=".storybook/preview.ts"
import { GrConfigProvider } from '@feugene/granularity/components/GrConfigProvider'
import { h } from 'vue'

export const decorators = [
  (story: () => unknown) => ({
    setup: () => () => h(GrConfigProvider, { size: 'md' }, () => h(story() as never)),
  }),
]
```

The provider renders transparently (`display: contents`) and does not change the
layout of a story, so it can be put on every story at once.

### Overlays

Modals, dropdowns and tooltips teleport into a shared portal — `#gr-portal` in
`body`, which the package creates itself on the client when the first teleporting
component renders. In Storybook that works with no setup: the preview has a
`body` of its own, and the portal appears in it.

Setup is needed only if you name a container of your own with `portalTarget`.
Then the requirement on it is the same as in an application, and it is a hard
one: **no `transform`, `filter`, `contain`, `perspective` or `will-change`** —
they create a containing block for `position: fixed`, and the panels start
computing their position from the container rather than from the viewport.
Storybook decorators create wrappers like that readily.

## Switching the theme

The theme is an attribute on the document root, and it is switched the same way
as in an application:

```ts title=".storybook/preview.ts"
export const globalTypes = {
  theme: {
    toolbar: {
      items: [
        { value: 'light', title: 'Light' },
        { value: 'dark', title: 'Dark' },
      ],
    },
  },
}

export const decorators = [
  (story: () => unknown, context: { globals: { theme?: string } }) => {
    document.documentElement.dataset.theme = context.globals.theme ?? 'light'
    return story()
  },
]
```

The decorators from both snippets live in a single `decorators` array: a second
`export const decorators` in the same file will not compile.

Both themes have to reach the CSS — `themes.names` in the config above is
responsible for that: without it granum takes the themes the core declares as
defaults, and that is `light` alone. Name only `light` there, or leave the field
out, and you get a switcher that changes an attribute and changes nothing else.

## Imports in stories

The auto-import resolver (`@feugene/unplugin-granularity`) works in SFC
templates. A story in TypeScript is not a template, and a component in it is
imported by its subpath:

```ts title="src/stories/Button.stories.ts"
import { GrButton } from '@feugene/granularity/components/GrButton'

export default { component: GrButton }

export const Primary = { args: { variant: 'primary', tone: 'primary' } }
```

A subpath rather than a root import: that is the whole point of granularity — one
component reaches the story bundle, not the package. granum also rejects a component
from the root as a build error, `ComponentRootImportError`.

## What to take from the portal instead of rewriting

- **Story arguments** — the API table on a component page is generated from the
  code, types and default values included. There is no point copying it into
  `argTypes` by hand: Storybook infers most of it from the SFC types itself.
- **Accessibility checks** — the `@feugene/granularity/testing` subpath gives an
  environment for mounting and the cleanup between tests, while axe and Playwright
  are plugged in by you. The details are on the
  [testing](/docs/testing) page. `@feugene/granularity-test-kit` is mostly about
  contract tests of the family's packages, but its `./a11y` (axe in jsdom) and
  `./e2e` (keyboard traversal and axe in Playwright) work for an application
  too.
- **Keyboard contracts** — they are described in the library repository per
  component, and a story does not replace them: they are about behaviour rather
  than about looks.
