Combining component variants in a Storybook Vue story
Reducing the number of Storybook stories improves the overview when browsing your product or design system documentation. When using tools like Chromatic, it reduces the number of snapshots and helps avoid the need to upgrade to a more expensive plan.
By using a relatively easy-to-setup Storybook decorator, you can render different variants of a component while keeping the simple structure where one story defines one component:
// utils/decorators.ts
import type { DecoratorFunction } from "storybook/internal/csf";
/**
* Renders multiple variants of a Story component in a column,
* given predefined args for each variant.
*
* Usage:
* const meta = {
* ...
* decorators: [createVariantDecorator([{ type: 'primary' }, { type:'secondary' }])]
* }
*/
export function createVariantDecorator<T>(variants: Partial<T>[]) {
return function (story, context) {
let Story = story();
// To be able to pass props to the <Story> component, the Story component
// needs to be an Object with a "render" function. Stories defined with
// only a 'meta' object, are not an object. Therefore, this condition is
// used to convert the story function to an object.
if (typeof Story === "function") {
Story = { render: Story };
}
return {
components: { Story },
data() {
return {
args: context.args,
variants,
};
},
template: `
<div class="flex flex-col gap-8 items-center">
<Story v-for="variant in variants" v-bind="{ ...args, ...variant }" />
</div>
`,
};
} as DecoratorFunction;
}
Use the decorator as follows:
// Button.stories.ts
import type { Meta, StoryObj } from "@storybook/vue3-vite";
import { createVariantDecorator } from "../../../utils/decorators";
import Button, { ButtonProps, ButtonTypes } from "./Button.vue";
const variantDecorator = createVariantDecorator<ButtonProps>([
{ type: ButtonTypes.Primary },
{ type: ButtonTypes.Secondary },
{ type: ButtonTypes.Tertiary },
{ type: ButtonTypes.Quaternary },
]);
const meta = {
title: "Button",
component: Button,
argTypes: {
type: {
control: "select",
options: Object.values(ButtonTypes),
},
label: { control: "text" },
},
args: {
label: "Button",
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// Single component story, great for property testing/configuring
export const Default: Story = {
args: {},
};
// All variants
export const Variants: Story = {
decorators: [variantDecorator],
};
// All variants in disabled state
export const Disabled: Story = {
args: { disabled: true },
decorators: [variantDecorator],
};
Currently this solution doesn’t work for components which use slots. Let me know if you have any feedback/suggestions.