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.
Machine-translated, not yet reviewed. Read the original
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, accessibility checks and visual regression. 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: compatibility is documented, 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 UnoCSS from the granular provider (installation). So the whole setup is about making UnoCSS run inside Storybook.
The build
Storybook on Vite (@storybook/vue3-vite) reads your vite.config.ts. If the
UnoCSS plugin is already there, there is nothing to add to .storybook/main.ts:
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
export default defineConfig({ plugins: [Vue(), UnoCSS()] })If Storybook has a config of its own, the plugin is added through viteFinal:
import UnoCSS from 'unocss/vite'
export default {
framework: '@storybook/vue3-vite',
stories: ['../src/**/*.stories.@(ts|tsx)'],
viteFinal: async config => ({
...config,
plugins: [...(config.plugins ?? []), UnoCSS()],
}),
}The styles
The same three imports as in the entry point of the application — in
preview.ts:
import '@unocss/reset/tailwind-compat.css'
import 'virtual:uno:granular.css'
import 'virtual:uno.css'The second import exists only with layer: 'granular' in the config. Without the
layer everything arrives in a single virtual:uno.css, and the extra import
breaks the build. Forgetting virtual:uno.css, on the other hand, gets you
components with tokens but without layout: the colours are there, the shape is
not.
The main trap: the extractor does not see dist
This is the first thing that breaks, and it breaks the same way for everyone: the stories rendered, and the components arrived without styles.
The cause is not Storybook. The utility classes the component templates are drawn
with live in the built dist of the package, and the UnoCSS extractor does not
look there by default — it scans the sources of your project. It has to be told:
import { defineConfig, presetMini } from 'unocss'
import { granularContent, presetGranularNode } from '@feugene/unocss-preset-granular/node'
import granularityProvider from '@feugene/granularity/granular-provider/node'
// One object for both calls: let them diverge and the extractor scans
// something other than what the preset generates.
const granular = {
providers: [granularityProvider],
themes: { names: ['light', 'dark'] },
layer: 'granular' as const,
}
export default defineConfig({
content: granularContent(granular),
presets: [presetMini(), presetGranularNode(granular)],
})content is read only from the top level of the config, not from the preset.
This is the same requirement as in an application — Storybook simply makes it
more visible, because a separate config is more often used there.
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:
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 first opening. 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:
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()
},
]Both themes have to reach the CSS — themes.names in the config above is
responsible for that. Name only light there 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:
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.
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
argTypesby hand: Storybook infers most of it from the SFC types itself. - Accessibility checks — the
@feugene/granularity/testingsubpath 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 page.@feugene/granularity-test-kitis not needed for this: it is about contract tests of the family’s packages rather than of an application. - 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.