Develops and documents UI components in isolation with Storybook's interactive workshop environment. Use when building component libraries, documenting design systems, or when user mentions Storybook, component documentation, or UI development.
Builds and documents UI components in Storybook's interactive workshop. Use when creating component libraries, documenting design systems, or when users mention Storybook, component documentation, or isolated UI development.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Industry-standard workshop for building, documenting, and testing UI components in isolation.
# Install in existing project
npx storybook@latest init
# Start Storybook
npm run storybook
.storybook/
main.ts # Configuration
preview.ts # Global decorators and parameters
preview-head.html # Custom head content
src/
components/
Button/
Button.tsx
Button.stories.tsx
Button.module.css
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
staticDirs: ['../public'],
typescript: {
reactDocgen: 'react-docgen-typescript',
},
};
export default config;
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import '../src/styles/globals.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1a1a1a' },
{ name: 'gray', value: '#f5f5f5' },
],
},
layout: 'centered',
},
decorators: [
(Story) => (
<div style={{ margin: '1rem' }}>
<Story />
</div>
),
],
globalTypes: {
theme: {
name: 'Theme',
description: 'Global theme',
defaultValue: 'light',
toolbar: {
icon: 'circlehollow',
items: ['light', 'dark'],
showName: true,
},
},
},
};
export default preview;
// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'danger'],
},
size: {
control: 'radio',
options: ['sm', 'md', 'lg'],
},
onClick: { action: 'clicked' },
},
};
export default meta;
type Story = StoryObj<typeof meta>;
// Stories
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Button',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Button',
},
};
export const Large: Story = {
args: {
size: 'lg',
children: 'Large Button',
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: 'Disabled',
},
};
export const InDarkMode: Story = {
decorators: [
(Story) => (
<div className="dark bg-gray-900 p-4">
<Story />
</div>
),
],
args: {
children: 'Dark Mode Button',
},
};
// Decorator with args access
export const WithContext: Story = {
decorators: [
(Story, context) => (
<ThemeProvider theme={context.globals.theme}>
<Story />
</ThemeProvider>
),
],
};
import { within, userEvent, expect } from '@storybook/test';
export const Filled: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Type in input
const input = canvas.getByPlaceholderText('Enter email');
await userEvent.type(input, 'test@example.com', { delay: 100 });
// Click button
const button = canvas.getByRole('button', { name: /submit/i });
await userEvent.click(button);
// Assert
await expect(canvas.getByText('Success!')).toBeInTheDocument();
},
};
export const WithValidation: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Submit empty form
await userEvent.click(canvas.getByRole('button'));
// Check for error
await expect(canvas.getByText('Email is required')).toBeVisible();
},
};
export const WithData: Story = {
loaders: [
async () => ({
users: await fetch('/api/users').then((r) => r.json()),
}),
],
render: (args, { loaded: { users } }) => (
<UserList users={users} {...args} />
),
};
// Or with render function
export const Loading: Story = {
render: () => {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
if (!data) return <Skeleton />;
return <Component data={data} />;
},
};
const meta: Meta<typeof Button> = {
argTypes: {
// Control types
variant: {
control: 'select',
options: ['primary', 'secondary'],
description: 'Visual style variant',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'primary' },
},
},
size: {
control: 'inline-radio',
options: ['sm', 'md', 'lg'],
},
disabled: {
control: 'boolean',
},
count: {
control: { type: 'number', min: 0, max: 100, step: 1 },
},
color: {
control: 'color',
},
date: {
control: 'date',
},
items: {
control: 'object',
},
label: {
control: 'text',
},
// Disable control
onClick: {
control: false,
},
// Categorize in table
theme: {
table: {
category: 'Styling',
},
},
},
};
{/* Button.mdx */}
import { Canvas, Meta, Story, Controls, ArgTypes } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';
<Meta of={ButtonStories} />
# Button
Buttons trigger actions when clicked.
## Usage
```tsx
import { Button } from '@/components/Button';
<Button variant="primary" onClick={handleClick}>
Click me
</Button>
// Button.tsx - JSDoc for autodocs
interface ButtonProps {
/** The visual style variant */
variant?: 'primary' | 'secondary' | 'danger';
/** Button size */
size?: 'sm' | 'md' | 'lg';
/** Whether the button is disabled */
disabled?: boolean;
/** Click handler */
onClick?: () => void;
/** Button content */
children: React.ReactNode;
}
/**
* Primary UI component for user interaction.
*
* @example
* ```tsx
* <Button variant="primary" onClick={handleClick}>
* Click me
* </Button>
* ```
*/
export function Button({ variant = 'primary', size = 'md', ...props }: ButtonProps) {
// ...
}
// Already included with @storybook/addon-essentials:
// - Controls: Interactive props
// - Actions: Event logging
// - Docs: Auto-documentation
// - Viewport: Responsive testing
// - Backgrounds: Background colors
// - Toolbars: Global controls
// - Measure: Layout measurement
// - Outline: DOM outlines
npm install -D @storybook/addon-a11y
// .storybook/main.ts
addons: ['@storybook/addon-a11y'],
// In story
export const Accessible: Story = {
parameters: {
a11y: {
config: {
rules: [{ id: 'color-contrast', enabled: true }],
},
},
},
};
npm install -D @storybook/addon-interactions @storybook/test
import { within, userEvent, expect, fn } from '@storybook/test';
const meta: Meta<typeof Form> = {
args: {
onSubmit: fn(),
},
};
export const Submitted: Story = {
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText('Email'), 'test@test.com');
await userEvent.click(canvas.getByRole('button'));
await expect(args.onSubmit).toHaveBeenCalled();
},
};
npm install -D @storybook/test-runner
# Run tests
npx test-storybook
# Watch mode
npx test-storybook --watch
// Button.test.tsx
import { composeStories } from '@storybook/react';
import { render, screen } from '@testing-library/react';
import * as stories from './Button.stories';
const { Primary, Secondary } = composeStories(stories);
test('Primary button renders correctly', () => {
render(<Primary />);
expect(screen.getByRole('button')).toHaveClass('btn-primary');
});
test('runs play function', async () => {
const { container } = render(<Secondary />);
await Secondary.play?.({ canvasElement: container });
});
# Install
npm install -D chromatic
# Run
npx chromatic --project-token=<token>
# .github/workflows/chromatic.yml
name: Chromatic
on: push
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
- run: npm ci
- uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
# Build static Storybook
npm run build-storybook
# Output in storybook-static/
# Deploy to GitHub Pages
# .github/workflows/deploy.yml
name: Deploy Storybook
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run build-storybook
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./storybook-static
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.