Histoire: Vue-First Component Story Tool Built on Vite

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-vue

Create 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-nuxt

nuxt.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:build

Outputs 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/playwright

Write 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.json

Key Points

  • Stories are .story.vue files 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 controls
  • setupFile with defineSetupVue3 provides 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

Read more