Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 099 / 100

Storybook and Histoire for Vue for Vue & Nuxt developers

Develop components in isolation.

Expona Admin8 min read

Introduction

By day 99 of your Vue and Nuxt journey, you've likely built dozens of components—buttons, modals, forms, cards—each existing somewhere in the codebase, waiting to be integrated into pages. But how do you develop, test, and document these components without constantly rebuilding your entire application? Storybook and Histoire for Vue for Vue & Nuxt developers offer a powerful answer: component-driven development in isolation.

In this post, we'll explore how to set up and use both tools to write stories for each component state, run visual tests, collaborate with designers, and integrate component testing into your CI pipeline.

Why Develop Components in Isolation?

Component-driven development means building UI components outside their natural application context. Instead of spinning up your full Nuxt app, navigating to a page, and triggering specific states manually, you document and render components in a controlled environment.

The benefits are tangible:

  • Faster feedback loops: change a component, see it instantly without rebuilding the app.
  • Complete state coverage: document edge cases—empty states, loading, error, success—systematically.
  • Living documentation: your components become self-documenting for new team members.
  • Designer collaboration: non-developers can review component variations without touching code.
  • Regression detection: visual snapshots catch unintended style changes.

Storybook is the industry standard, battle-tested across thousands of projects. Histoire is a lighter, Vue-native alternative that integrates seamlessly with Vite and feels more idiomatic in the Vue ecosystem.

Write Stories for Each State

A story is a snapshot of your component in a specific state. For a button component, you might have stories for primary, secondary, disabled, and loading states. For a form input, stories cover empty, filled, error, and focused states.

Here's how to structure stories in Storybook for Vue 3:

typescript
// Button.stories.ts
import { Meta, StoryObj } from '@storybook/vue3'
import Button from './Button.vue'

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'danger'],
    },
    size: {
      control: { type: 'select' },
      options: ['sm', 'md', 'lg'],
    },
    disabled: { control: 'boolean' },
    loading: { control: 'boolean' },
  },
}

export default meta
type Story = StoryObj<typeof meta>

export const Primary: Story = {
  args: {
    variant: 'primary',
    size: 'md',
    disabled: false,
    loading: false,
    label: 'Click me',
  },
}

export const PrimaryLoading: Story = {
  args: {
    variant: 'primary',
    size: 'md',
    disabled: true,
    loading: true,
    label: 'Processing...',
  },
}

export const Secondary: Story = {
  args: {
    variant: 'secondary',
    size: 'md',
    disabled: false,
    loading: false,
    label: 'Cancel',
  },
}

export const Danger: Story = {
  args: {
    variant: 'danger',
    size: 'md',
    disabled: false,
    loading: false,
    label: 'Delete',
  },
}

export const AllSizes: Story = {
  render: (args) => ({
    components: { Button },
    setup() {
      return { args }
    },
    template: `
      <div class="flex gap-4">
        <Button v-bind="{ ...args, size: 'sm' }" label="Small" />
        <Button v-bind="{ ...args, size: 'md' }" label="Medium" />
        <Button v-bind="{ ...args, size: 'lg' }" label="Large" />
      </div>
    `,
  }),
}

For Histoire, the syntax is simpler and more Vue-friendly:

typescript
// Button.story.vue
<template>
  <Story
    title="Components/Button"
    :layout="{ type: 'grid', width: '100px' }"
  >
    <Variant title="Primary">
      <Button variant="primary" label="Click me" />
    </Variant>

    <Variant title="Primary Loading">
      <Button variant="primary" label="Processing..." loading />
    </Variant>

    <Variant title="Secondary">
      <Button variant="secondary" label="Cancel" />
    </Variant>

    <Variant title="Danger">
      <Button variant="danger" label="Delete" />
    </Variant>

    <Variant title="All Sizes">
      <div class="flex gap-4">
        <Button size="sm" label="Small" />
        <Button size="md" label="Medium" />
        <Button size="lg" label="Large" />
      </div>
    </Variant>
  </Story>
</template>

<script setup lang="ts">
import { Story, Variant } from 'histoire/components'
import Button from './Button.vue'
</script>

Key principle: one story per meaningful state. If your component has boolean flags or enum options, create distinct stories for each combination that matters in production. This prevents the "I didn't know this state existed" conversation six months later.

Visual Testing and Docs

Once stories are written, you can run Storybook or Histoire locally and interact with your components in a browser. Both tools offer:

  • Interactive controls: adjust props with dropdowns, toggles, and text inputs without editing code.
  • Responsive preview: view components at different viewport sizes.
  • Addon ecosystem: accessibility audits, viewport testing, theme switching.
  • Auto-generated docs: JSDoc comments automatically populate documentation panels.

To document your Button component, add comments to your code:

typescript
// Button.vue
<template>
  <button
    :class="['btn', `btn-${variant}`, `btn-${size}`]"
    :disabled="disabled || loading"
    @click="$emit('click')"
  >
    <span v-if="loading" class="spinner" />
    {{ label }}
  </button>
</template>

<script setup lang="ts">
/**
 * Primary button component for user interactions.
 * Supports multiple variants and sizes with built-in loading state.
 *
 * @example
 * <Button variant="primary" size="md" label="Submit" />
 */
interface Props {
  /** Visual style: primary, secondary, or danger */
  variant?: 'primary' | 'secondary' | 'danger'
  /** Button size: sm, md, or lg */
  size?: 'sm' | 'md' | 'lg'
  /** Disable interaction and show loading spinner */
  loading?: boolean
  /** Standard HTML disabled attribute */
  disabled?: boolean
  /** Button text content */
  label: string
}

withDefaults(defineProps<Props>(), {
  variant: 'primary',
  size: 'md',
  loading: false,
  disabled: false,
})

defineEmits<{
  click: []
}>()
</script>

Storybook automatically generates a Docs tab from these comments. For Histoire, add a `description` field to your story variant:

typescript
<Variant
  title="Primary"
  description="The default button style for primary actions. Use this for the most important user action on the page."
>
  <Button variant="primary" label="Click me" />
</Variant>

Share with Designers

Designers benefit from a published component library. Both Storybook and Histoire can be deployed to a static host:

typescript
// package.json scripts
{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "storybook:build": "storybook build -o dist/storybook",
    "histoire": "histoire dev",
    "histoire:build": "histoire build"
  }
}

Deploy the built output to Netlify, Vercel, or S3. Share the URL with your design team. They can:

  • Review component variations without context-switching to code review platforms.
  • Test responsive behavior and edge cases before handing off to QA.
  • Suggest changes via comments directly on component previews (Chromatic, a Storybook companion service, enables this).
  • Validate that implemented components match design intent.

For Nuxt projects, organize your stories alongside your component library. If you have a components/ directory, create components/__stories__/ or use a naming convention like Button.story.vue for Histoire or Button.stories.ts for Storybook.

Run in CI

To prevent regressions and catch accidental style changes, integrate component testing into your CI pipeline. Both tools support this:

Storybook with Chromatic:

typescript
// .github/workflows/chromatic.yml
name: Chromatic
on: [push, pull_request]

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run storybook:build
      - uses: chromaui/action@v1
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          storybookBuildDir: dist/storybook

Chromatic automatically captures visual snapshots and alerts you to changes. Non-breaking changes require approval; breaking changes block the PR.

Histoire with built-in snapshot testing:

typescript
// package.json
{
  "scripts": {
    "test:stories": "histoire test"
  }
}

This runs visual regression tests headlessly using Playwright. Integrate it into your CI:

typescript
// .github/workflows/tests.yml
name: Tests
on: [push, pull_request]

jobs:
  stories:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run test:stories
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: histoire-report
          path: .histoire/

On first run, tests generate baseline snapshots. On subsequent runs, they compare rendered components against baselines. Any visual drift fails the build, forcing explicit review and approval before merge.

Common Mistakes

  • Treating stories as unit tests: stories document UI, not logic. Use Vitest or Jest for unit tests. Stories complement, don't replace, test coverage.
  • Over-complicating variants: avoid creating stories for every possible prop combination. Focus on states that matter: success, error, loading, empty. Let interactive controls handle minor variations.
  • Neglecting accessibility: enable Storybook's a11y addon or run Histoire with accessibility checks enabled. Stories are perfect places to audit ARIA labels and contrast ratios.
  • Ignoring mobile viewports in stories: use viewport addons to test responsive variants. A beautiful desktop button means nothing if it breaks on mobile.
  • Letting stories drift from components: if you change a component prop, update its stories immediately. Stale stories mislead future developers and waste time in code review.

Key Takeaways

  • Stories document component states systematically. One story per meaningful state prevents surprises in production.
  • Storybook is the industry standard with a mature addon ecosystem; Histoire is lighter and more Vue-idiomatic, ideal for teams embracing Vite and modern tooling.
  • Visual testing in CI catches regressions automatically. Chromatic for Storybook, Histoire's built-in snapshot tests, or Playwright integration prevent accidental breakage.
  • Published component libraries become a shared language between engineers and designers, reducing context-switching and aligning implementation with design intent.
  • Stories live alongside components, not in a separate folder. Colocation makes it natural to update both together and reduces the cognitive overhead of maintaining documentation.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles