Histoire: Vue-First Component Story Tool Built on Vite
Histoire is a component story tool for Vue 3 (and experimentally Svelte). Where Storybook adapted its React-first design to Vue, Histoire was built with Vue 3 in mind from the start. It uses Vite natively, integrates with the Vue SFC format naturally, and renders stories in an iframe-less environment that preserves Vue's reactivity. Startup is fast, and the story authoring model uses .story.vue files rather than JS/TS exports.
Why Histoire?
- Vue 3 native: Stories use Vue SFCs, not JS object exports
- Vite-native: Fast HMR and build, same Vite config as your app
- No iframe: Stories render in the same context as your app — no cross-frame communication overhead
- Nuxt support: First-class Nuxt 3 integration
Installation
npm install --save-dev histoire @histoire/plugin-vueCreate histoire.config.ts:
import { defineConfig } from 'histoire';
import { HstVue } from '@histoire/plugin-vue';
export default defineConfig({
plugins: [HstVue()],
setupFile: './src/histoire-setup.ts', // optional
});Add scripts:
{
"scripts": {
"story:dev": "histoire dev",
"story:build": "histoire build",
"story:preview": "histoire preview"
}
}Writing Stories
Stories are .story.vue files:
<!-- Button.story.vue -->
<script setup lang="ts">
import { reactive } from 'vue'
import Button from './Button.vue'
const state = reactive({
label: 'Click me',
variant: 'primary' as 'primary' | 'secondary' | 'danger',
disabled: false,
})
</script>
<template>
<Story title="Button" :layout="{ type: 'centered', iframe: false }">
<Variant title="Default">
<Button :variant="state.variant" :disabled="state.disabled">
{{ state.label }}
</Button>
</Variant>
<Variant title="Disabled">
<Button variant="primary" disabled>Disabled</Button>
</Variant>
<Variant title="Danger">
<Button variant="danger">Delete</Button>
</Variant>
</Story>
</template>Multiple <Variant> blocks inside <Story> create separate tabs in the Histoire UI — each variant is a distinct visual state.
Controls with <HstText>, <HstCheckbox>, <HstSelect>
Histoire ships built-in control components. Add them to the story's control panel:
<script setup lang="ts">
import { reactive } from 'vue'
import { logEvent } from 'histoire/client'
import Button from './Button.vue'
const state = reactive({
label: 'Click me',
variant: 'primary',
disabled: false,
})
</script>
<template>
<Story title="Button">
<template #controls>
<HstText v-model="state.label" title="Label" />
<HstSelect
v-model="state.variant"
title="Variant"
:options="['primary', 'secondary', 'danger']"
/>
<HstCheckbox v-model="state.disabled" title="Disabled" />
</template>
<Variant title="Interactive">
<Button
:variant="state.variant"
:disabled="state.disabled"
@click="logEvent('click', $event)"
>
{{ state.label }}
</Button>
</Variant>
</Story>
</template>logEvent logs events to the Histoire event log panel — equivalent to the actions addon in Storybook.
Global Setup
For providers, Pinia, Vue Router, or global components, use setupFile:
// src/histoire-setup.ts
import { defineSetupVue3 } from '@histoire/plugin-vue'
import { createPinia } from 'pinia'
import { router } from './router'
import './assets/styles.css'
export const setupVue3 = defineSetupVue3(({ app, story, variant }) => {
const pinia = createPinia()
app.use(pinia)
app.use(router)
})Every story's Vue app instance gets this setup applied automatically.
Nuxt Integration
Histoire has a Nuxt module:
npm install --save-dev @histoire/plugin-nuxtnuxt.config.ts:
export default defineNuxtConfig({
modules: ['@histoire/plugin-nuxt'],
histoire: {
// Histoire config here
},
})With Nuxt integration, Histoire automatically picks up your Nuxt plugins, composables, and auto-imports — stories behave like Nuxt pages.
Layout Options
Control how stories are displayed:
<!-- Full viewport (default) -->
<Story :layout="{ type: 'single' }">
<!-- Centered in a fixed area -->
<Story :layout="{ type: 'centered', width: 400 }">
<!-- Side-by-side grid of variants -->
<Story :layout="{ type: 'grid', width: 200 }">Grid layout is useful for icon libraries, color swatches, and badge variants — you see all variants at once.
Source Code Display
Histoire automatically displays the source code for each variant. Enable it:
<Variant title="Primary" :source="true">
<Button variant="primary">Click me</Button>
</Variant>Or customize the displayed source:
<Variant
title="Primary"
source="<Button variant='primary'>Click me</Button>"
>
<Button variant="primary">Click me</Button>
</Variant>Generating Static Documentation
npm run story:buildOutputs to histoire-dist/ by default. Deploy to any static host for shareable component docs.
Set a base URL for subdirectory deployment:
// histoire.config.ts
export default defineConfig({
outDir: 'histoire-dist',
base: '/components/',
})Visual Regression Integration
Histoire integrates with visual testing tools by capturing story screenshots. With Argos CI:
npm install --save-dev @argos-ci/playwrightWrite a Playwright test that opens each story:
import { test } from '@playwright/test';
import { argosScreenshot } from '@argos-ci/playwright';
test('Button stories', async ({ page }) => {
await page.goto('http://localhost:6006/story/src-button-story-vue--default-primary');
await argosScreenshot(page, 'button-primary');
await page.goto('http://localhost:6006/story/src-button-story-vue--default-disabled');
await argosScreenshot(page, 'button-disabled');
});Or use the Histoire CLI to enumerate all stories and loop:
histoire collect --json > stories.jsonKey Points
- Stories are
.story.vuefiles using Vue SFCs — natural for Vue developers, no JS object exports <Story>contains<Variant>blocks for different component states<template #controls>with<HstText>,<HstSelect>,<HstCheckbox>adds interactive controlssetupFilewithdefineSetupVue3provides Pinia, Vue Router, and global components to all stories- First-class Nuxt 3 support via
@histoire/plugin-nuxt - Grid layout for mass variant display (icons, swatches); centered layout for isolated components
- Deploy as static docs with
histoire build