Components Book Module for Nuxt

Overview
This module provides a Storybook-like experience for Nuxt components, allowing you to document and test your Vue components using .stories.vue
files. It automatically scans a specified directory for story files, generates dynamic routes, and creates an interactive UI for viewing and testing components.
Unlike Storybook, which can be complex and heavy, this module is lightweight and seamlessly integrates into Nuxt, making it easy to set up and use. All stories are written as standard Vue components, ensuring a smooth and intuitive development experience.

Features
- 📦 Automatic scanning of
.stories.vue
files and registration as pages. - ⚡ Live reloading with file-watching support.
- 🛠 Extracts and displays component props dynamically.
- 🏗 Nuxt DevTools Integration for quick access.
- 📋 Built-in component previewing with
EnhancedPreview
. - 🔄 Supports dynamic prop manipulation and slot usage.
- 🚀 Flexible component embedding with event handling support.
Installation
npm install --save-dev nuxt-componentsbook-module
or
yarn add --dev nuxt-componentsbook-module
Setup
1. Register the Module
Add the module to your nuxt.config.ts
:
export default defineNuxtConfig({
modules: [
'nuxt-componentsbook-module',
],
componentsBook: {
componentsDir: 'components',
disabled: false,
cache: true,
},
})
2. Creating a Story
To document a component, create a .stories.vue
file in your components directory:
MyInput.stories.vue (Example with EnhancedPreview
)
<script setup>
import { ref } from '#imports'
import CustomInput from './MyInput.vue'
const modelValue = ref('')
const label = ref('Enter Text')
const type = ref<'text' | 'password' | 'email' | 'number'>('text')
const placeholder = ref('Type something...')
const disabled = ref(false)
const readonly = ref(false)
const helperText = ref('This is a helper text.')
const size = ref<'sm' | 'md' | 'lg'>('md')
</script>
<template>
<h1>🟢 CustomInput Component</h1>
<p>
The <code>CustomInput</code> component is a versatile input field with multiple configurations.
</p>
<h2>🛠 Interactive Controls</h2>
<div class="controls">
<label>
Label:
<input v-model="label" type="text">
</label>
<label>
Type:
<select v-model="type">
<option value="text">Text</option>
<option value="password">Password</option>
<option value="email">Email</option>
<option value="number">Number</option>
</select>
</label>
</div>
<h2>🔹 Preview</h2>
<EnhancedPreview
v-model="modelValue"
:component="CustomInput"
:props="{ label, type, placeholder, disabled, readonly, 'helper-text': helperText, size }"
:emits="['click']"
@click="console.log('Clicked!')"
/>
</template>
3. Running the Components Book
Start your Nuxt development server:
npm run dev
Visit /componentsbook
in your browser to see the list of stories.
📌 Using EnhancedPreview
The EnhancedPreview
component is the recommended way to embed and test your components interactively. It allows for dynamic prop manipulation, event handling, and slot usage.
Example Usage
<EnhancedPreview
v-model="modelValue"
:component="CustomInput"
:props="{
label: 'Enter Text',
type: 'text',
placeholder: 'Type something...',
disabled: false,
readonly: false,
'helper-text': 'This is a helper text.',
size: 'md',
}"
:emits="['click']"
@click="handleClick"
>
<template #append>
test slot
</template>
</EnhancedPreview>
Because it is so flexible, you can create a near-complete “in-component Storybook experience,” connecting your state management (Vuex, Pinia, or custom Refs/Reactives) and a wide range of events to a single preview component.
Key Features of EnhancedPreview
- Supports
v-model
: Automatically binds v-model
values. - Handles events dynamically: Passes events such as
@click
, @hover
, and custom events. - Slot support: Allows injecting content into component slots.
- Live preview: Updates props and re-renders instantly.
- Code generation: Displays and copies usage examples.
Enhanced usage(useEnhancedPreview)
Below is an advanced example of how you can leverage useEnhancedPreview
to gain complete control over a component’s state, events, and display. This approach requires a bit more work, but it allows you to integrate fully custom store logic or any other advanced patterns you need.
Overview
The useEnhancedPreview
composable is an extended utility that goes beyond a simple component preview. It provides you with:
- Reactive props and
v-model
binding - Event forwarding (
emits
) - Additional event listeners (e.g.,
onClick
, onFocus
, etc.) - Slot serialization (for generating code snippets)
- Code copying / freezing features
Signature
useEnhancedPreview(
props: UseEnhancedPreviewProps,
emit: (event: string, ...args: unknown[]) => void,
)
Parameters
props
(object) includes the following fields:
Field | Type | Description |
---|
component | DefineComponent | string | unknown | The main component to render. This can be: 1) A string representing a native HTML tag (e.g., "button" ) 2) A Vue component (e.g., markRaw(MyComponent) ) 3) A dynamic reference to a component loaded at runtime. |
modelValue | string | number | boolean | object | array | null | Ref<unknown> | Value used for two-way data binding. If defined, the composable automatically sets up the v-model logic (i.e., modelValue + onUpdate:modelValue ). This can be a ref or a direct value. |
props | Record<string, unknown> | Additional props that should be passed to the rendered component. This can include normal props or specialized keys like 'v-model:checked' , 'v-model:foo' , etc. The composable internally wires these up to update events. |
emits | string[] | An array of event names that the component might emit. If you list ['click', 'myEvent'] , for example, the composable will handle them via its internal emitEvent logic. You can also see these events reflected in the generated code snippet. |
listeners | Record<string, (...args: unknown[]) => void> | A dictionary of additional event handlers. This can be either: - Keys without on prefix, e.g. { click: () => {...} } - Keys with on prefix, e.g. { onClick: () => {...} } . These listeners are attached directly to the rendered component in Vue 3 style (onClick , onFocus , etc.). |
emit
is a function with the signature:
(event: string, ...args: unknown[]) => void
Typically this is defineEmits
from within the parent <script setup>
.
Returned Properties
The composable returns a set of reactive values and computed properties that you can integrate into your template:
Property | Type | Description |
---|
renderedComponent | ComputedRef<VNode> | The actual Vue node that you can render via <component :is="renderedComponent" /> . It combines all the props, events, and listeners into a single component instance. |
dynamicProps | ComputedRef<Record<string, unknown>> | Internal object of all processed props. You usually won’t render this directly, but it’s accessible if you need to debug or pass them somewhere else. |
generatedCode | ComputedRef<string> | An auto-generated code snippet that shows how to use the component with the currently bound props, events, and (optionally) slot placeholders. This can be displayed to the user or used for copying to the clipboard. |
copyButtonText | Ref<string> | The text on a “Copy” button. It updates automatically to ✅ Copied! when the user copies the snippet, then reverts back to 📋 Copy . |
isFrozen | Ref<boolean> | Indicates whether the code snippet is “frozen.” When frozen, the generatedCode no longer reacts to prop changes. Useful for capturing a stable snippet even while you continue changing the actual component’s props in the UI. |
toggleFreeze | () => void | Toggles the isFrozen state. If unfrozen, calling toggleFreeze captures the current code snippet and stops future updates. If frozen, calling it again releases the freeze. |
copyCode | () => Promise<void> | Copies the current generatedCode to the user’s clipboard. Sets copyButtonText to “✅ Copied!” for a few seconds as feedback. |
Advanced Examples
Below is a comprehensive example of how to integrate the composable. It demonstrates:
- Marking a component as
markRaw
to avoid Vue reactivity overhead. - Using
reactive
to handle multiple fields and watchers within a single object. - Providing
listeners
for custom event handling. - Using
emits
to specify which events should be recognized and forwarded.
Example: Textarea + Badge
<script setup lang="ts">
import CustomTextarea from './CustomTextarea.vue'
import CustomBadge from './CustomBadge.vue'
import { ref, reactive, markRaw } from '#imports'
import { useEnhancedPreview } from '@/runtime/composables/useEnhancedPreview'
// We have a text area and a badge, each with their own props
const modelValue = ref('')
const placeholder = ref('Type here...')
const text = ref('Badge Label')
const variant = ref<'primary' | 'secondary'>('primary')
// Additional handler just to show we can do custom logic
const handleInput = () => {
console.log('Text entered:', modelValue.value)
}
// Define an `emit` for v-model updates or custom emits
const emit = defineEmits(['update:modelValue'])
// 1) Setup for CustomTextarea
const {
copyButtonText,
isFrozen,
toggleFreeze,
copyCode,
renderedComponent,
generatedCode,
} = useEnhancedPreview(
reactive({
component: markRaw(CustomTextarea), // Mark the component as raw
modelValue, // Pass a ref directly
props: {
placeholder: placeholder.value, // Normal prop
},
emits: ['update:modelValue'], // We'll forward this event
listeners: {
// You can use either `update:modelValue` or `onUpdate:modelValue`
'update:modelValue': (value) => {
modelValue.value = value as string
},
},
}),
emit as (event: string, ...args: unknown[]) => void
)
// 2) Setup for CustomBadge
const {
copyButtonText: copyButtonTextBadge,
isFrozen: isFrozenBadge,
toggleFreeze: toggleFreezeBadge,
copyCode: copyCodeBadge,
renderedComponent: renderedBadge,
generatedCode: generatedCodeBadge,
} = useEnhancedPreview(
reactive({
component: markRaw(CustomBadge),
// No need for modelValue here; just passing some props
props: {
text: text.value,
variant: variant.value,
},
}),
emit as (event: string, ...args: unknown[]) => void
)
</script>
<template>
<!-- Render the Textarea -->
<p>
The <code>CustomTextarea</code> component provides a multi-line text input.
</p>
<component :is="renderedComponent" />
<PreviewSpoiler>
<PreviewCodeBlock
:code="generatedCode"
:show-frozen="true"
:is-frozen="isFrozen"
:copy-button-text="copyButtonText"
@toggle-freeze="toggleFreeze"
@copy="copyCode"
/>
</PreviewSpoiler>
<!-- Render the Badge -->
<component :is="renderedBadge" @update:model-value="handleInput" />
<PreviewSpoiler>
<PreviewCodeBlock
:code="generatedCodeBadge"
:show-frozen="true"
:is-frozen="isFrozenBadge"
:copy-button-text="copyButtonTextBadge"
@toggle-freeze="toggleFreezeBadge"
@copy="copyCodeBadge"
/>
</PreviewSpoiler>
</template>
Explanation
-
markRaw(CustomTextarea)
We wrap our Vue component in markRaw()
so that Vue does not convert the component object into a reactive proxy. This prevents warnings and extra overhead.
-
Using reactive(...)
We pass an object that bundles up our refs (modelValue
) and literal values (props
) together. This allows them to be watched for changes. The composable will reflect those changes in the code snippet automatically.
-
listeners
In the first setup, we provide a listener for 'update:modelValue'
. This ensures that whenever CustomTextarea
emits that event, we update modelValue.value
accordingly.
- Alternatively, you could have used
'onUpdate:modelValue'
or 'onClick'
if you prefer the Vue 3 naming style.
-
Multiple Instances
We show useEnhancedPreview
used twice — once for the textarea, once for the badge. Each instance returns a unique set of computed properties and reactive states.
-
Rendering
Instead of writing <CustomTextarea v-model="modelValue" :placeholder="placeholder" />
, you simply do:
<component :is="renderedComponent" />
The composable already merges the props, the v-model
logic, and the event listeners for you.
Additional Notes
Recommendations
- Start with simple usage (just the
component
prop, maybe a modelValue
) before introducing advanced store logic or multiple watchers. - Always wrap large or complex component objects with
markRaw()
if you pass them to useEnhancedPreview
in a reactive context. - If you only need standard props and events, consider the simpler usage with
EnhancedPreview
in .stories.vue
. This advanced integration is primarily for scenarios where you need deeper control.
Summary
useEnhancedPreview
is an advanced composable that provides a flexible, high-powered way to preview your components with interactive props, event forwarding, and snippet generation. It’s ideal when you need a level of control that goes beyond simple in-component previews, such as fully custom store integrations or specialized event handling.
By carefully configuring props
, modelValue
, emits
, and listeners
, you can build a robust, dynamic “mini Storybook” experience directly within your Nuxt app.
How It Works
- The module scans the specified
componentsDir
for .stories.vue
files. - Generates dynamic Vue pages for each story and registers them with Nuxt.
- Provides a UI layout for previewing and testing components interactively.
- Supports real-time editing with automatic updates when files are modified.
- Enhances DevTools, adding a new tab called Components Book.
nuxt-i18n-micro integration
Below is an updated note clarifying why you should place nuxt-componentsbook-module
before nuxt-i18n-micro
in your modules
array, given that in this specific setup, having the locale prefix added to Components Book routes can cause problems.
Integration with nuxt-i18n-micro
Example nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config'
export default defineNuxtConfig({
extends: './basic',
modules: [
'nuxt-componentsbook-module',
'nuxt-i18n-micro',
],
})
Why the order matters here
- When
nuxt-i18n-micro
is after the Components Book module, it will attempt to apply locale prefixes (e.g., /en/
) to the already-registered Components Book routes. - In this particular setup, adding the prefix can break your story routes (for example,
/en/componentsbook/...
might conflict with how the Components Book module is generating or managing its pages). - By listing
nuxt-componentsbook-module
first and nuxt-i18n-micro
second, you avoid having a locale prefix automatically prepended to the Components Book routes, which prevents potential route conflicts.
🛠 DevTools Integration
When running in development mode, a Components Book tab appears in Nuxt DevTools, providing an iframe-based UI for exploring stories.
More Resources
- Live Demo – Explore the module in action and see various sample stories.
- Usage Examples – View additional
.stories.vue
files illustrating different configurations and patterns.
You can use these references to learn more advanced usage patterns, get inspired by existing stories, and see how they integrate with the rest of your Nuxt app.
🤝 Contributing
Feel free to submit issues and pull requests to improve the module.
📜 License
MIT License