Introduction
Working with frontend in Drupal has traditionally been tightly coupled with the CMS. This means that building, testing, and iterating on UI components often requires a full Drupal environment running behind it — making the feedback loop slow and painful for frontend developers.
Storybook offers a way out of this loop. It provides an isolated sandbox where you can develop, test, and document your components entirely outside of Drupal, without needing a running backend. With the introduction of Single Directory Components (SDC) in Drupal 10.1+, this approach has become even more natural. SDC bundles everything a component needs — Twig template, CSS, JavaScript, and a schema definition — into a single self-contained directory.
In this guide, we'll walk through how to set up Storybook in a Drupal project, write stories for your components, and integrate them with SDC so your theme system stays consistent, testable, and maintainable.
Why This Combination?
- Frontend Independence — Storybook decouples your UI work from Drupal's render pipeline. Frontend developers no longer need to clear caches, navigate to specific nodes, or mock content entities just to see a button variant.
- Living Documentation — Each story acts as a visual spec. New developers can browse your component library without reading a single Twig file or knowing anything about Drupal's render arrays.
- Consistency Across Teams — Designers, developers, and QA can reference the same component library. What you see in Storybook is what you should see in Drupal.
- Built-in Testing — Storybook integrates with visual regression tools (like Chromatic), accessibility checkers, and interaction tests — giving your Drupal frontend a testing layer it usually lacks.
Prerequisites
- Drupal 10.1+ (for SDC support)
- Node.js 18+
- npm or Yarn
- A custom Drupal theme (we'll call it
mytheme)
Step 1: Initialize Storybook
Navigate to your theme directory and run the Storybook initializer:
cd web/themes/custom/mytheme
npx storybook@latest init --type html
This scaffolds a .storybook/ config directory and a stories/ folder. Since Drupal uses Twig (not React or Vue), we initialize with the html type and will add Twig rendering later.
After initialization, verify it works:
npm run storybook
Step 2: Add Twig Support
Storybook doesn't natively understand Twig. To bridge this gap, we use twig-loader and the TwigJS rendering engine.
npm install --save-dev twig-loader twig
Then, update your Storybook Webpack configuration. Create or modify .storybook/main.js:
module.exports = {
stories: [
'../components/**/*.stories.@(js|jsx|ts|tsx)',
],
framework: '@storybook/html',
webpackFinal: async (config) => {
config.module.rules.push({
test: /\.twig$/,
use: 'twig-loader',
});
return config;
},
};
Step 3: Create an SDC Component
Single Directory Components follow a strict directory structure. Each component lives in a self-contained folder under components/:
└── components/
└── button/
├── button.twig
├── button.css
└── button.component.yml
button.component.yml
The schema file that defines the component's API:
name: Button
status: stable
props:
type: object
properties:
label:
type: string
title: Label
description: The text inside the button
default: "Click Me"
variant:
type: string
title: Variant
enum:
- primary
- secondary
- outline
default: primary
button.twig
<button class="btn btn--{{ variant|default('primary') }}">
{{ label|default('Click Me') }}
</button>
button.css
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.btn--primary {
background-color: #0057ff;
color: white;
}
.btn--secondary {
background-color: #6c757d;
color: white;
}
.btn--outline {
background-color: transparent;
border: 2px solid #0057ff;
color: #0057ff;
}
Step 4: Write a Story
Create a story file at components/button/button.stories.js:
import buttonTemplate from './button.twig';
import './button.css';
export default {
title: 'Components/Button',
argTypes: {
label: { control: 'text', defaultValue: 'Click Me' },
variant: {
control: { type: 'select' },
options: ['primary', 'secondary', 'outline'],
defaultValue: 'primary',
},
},
};
const Template = (args) => buttonTemplate(args);
export const Primary = Template.bind({});
Primary.args = { label: 'Primary Button', variant: 'primary' };
export const Secondary = Template.bind({});
Secondary.args = { label: 'Secondary', variant: 'secondary' };
export const Outline = Template.bind({});
Outline.args = { label: 'Outline', variant: 'outline' };
Step 5: Scale It — Complex Components
Once the basics work, you can apply the same pattern to more complex components. Here's a Card component example:
└── components/
└── card/
├── card.twig
├── card.css
├── card.component.yml
└── card.stories.js
card.twig
<article class="card {{ modifier_class|default('') }}">
{% if image %}
<div class="card__image">
<img src="{{ image }}" alt="{{ image_alt|default('') }}">
</div>
{% endif %}
<div class="card__content">
<h3 class="card__title">{{ title }}</h3>
{% if body %}
<p class="card__body">{{ body }}</p>
{% endif %}
{% if cta_label %}
<a href="{{ cta_url|default('#') }}" class="card__cta">{{ cta_label }}</a>
{% endif %}
</div>
</article>
Keeping Drupal and Storybook in Sync
The key to maintaining parity between Drupal and Storybook is treating your SDC components as the single source of truth. Here's how:
- Never hardcode styles in templates — Always reference your component CSS.
- Use the
.component.ymlschema as a contract — Both Drupal's render engine and your Storybook stories should reference the same property set. - Automate visual regression — Use Chromatic or Percy to catch visual drift between what's in Storybook and what ships to production.
- Reference design tokens — If you're using a design system, centralize your CSS variables and import them in both Storybook's preview and your Drupal theme.
Conclusion
Setting up Storybook with SDC in Drupal is not just a nice-to-have — it's a fundamental shift in how you approach frontend architecture. By isolating your components, you gain:
- Faster development cycles
- Better documentation
- Real testing infrastructure for your frontend
- Cleaner collaboration between designers and developers
The effort to set it up pays for itself quickly, especially in teams that maintain large component libraries or need to iterate frequently on UI without waiting for backend deployments.