Use Storybook 9 with ClojureScript!
See the examples directory in this Git repo for working examples.
- Node.js
- shadow-cljs
- A ClojureScript+React project using either HSX or UIx
npx @factorhouse/storybook-cljs init
The init
command will install the required dependencies to your package.json
file and initialize the required project structure in the .storybook/
directory.
You will need to add a :storybook
shadow-cljs build profile:
{:builds {:storybook {:target :npm-module
:entries [todomvc.stories]
:output-dir ".storybook/cljs-out/"
:build-hooks [(io.factorhouse.storybook.compiler/configure
{:compiler io.factorhouse.storybook.compiler.uix})
(io.factorhouse.storybook.compiler/compile)]}}}
- Target:
:npm-module
- Output directory:
.storybook/cljs-out/
- Storybook build hook:
- Specify the
:compiler
as eitherio.factorhouse.storybook.compiler.hsx
orio.factorhouse.storybook.compiler.uix
- Specify the
Add to project.clj
or deps.edn
:
[io.factorhouse/storybook-cljs "0.2.0"]
(ns example.core
(:require [io.factorhouse.storybook.core :as storybook]
[uix.core :refer [$ defui]]))
(defui button [{:keys [variant size children] :or {variant :primary size :md}}]
($ :button {} children))
(defmethod storybook/story "Component/Buttons/Secondary" [_]
{:component button
:stories {:Default {:args {:variant :secondary :children ["Secondary Button"]}}
:Large {:args {:variant :secondary :size :lg :children ["Large Secondary"]}}
:WithIcon {:args {:variant :secondary :children ["Secondary"]}}}))
- UIx - see examples/uix for details
- HSX - see examples/hsx for details
npx shadow-cljs watch storybook
npx storybook dev
npx shadow-cljs release storybook
npx storybook build
Your Storybook project will get compiled to ./storybook-static/
Use the io.factorhouse.storybook.core/story
multi-method to define component stories for your Storybook.
You can define multiple story hierarchies in the same CloujreScript namespace:
(defmethod storybook/story "Component/Buttons/Primary" [_] ...)
(defmethod storybook/story "Component/Buttons/Secondary" [_] ...)
(defmethod storybook/story "Component/Buttons/Variants" [_] ...)
Stories should be organized in a dev-src
directory to keep development-only code separate from production source:
project/
├── src/ ; Production source code
│ └── components/
├── dev-src/ ; Development-only stories
│ ├── stories.cljs ; Central namespace for collecting stories
│ └── stories/
│ ├── buttons.cljs
│ ├── forms/
│ │ └── inputs.cljs
│ └── layout/
│ └── cards.cljs
└── shadow-cljs.edn
Important: Your shadow-cljs.edn
:entries
must include the central namespace (e.g., dev-src.stories
) that requires all story namespaces to ensure multi-methods are loaded at runtime.
;; dev-src/stories.cljs - Central collection namespace
(ns stories
(:require [stories.buttons.primary]
[stories.buttons.secondary]
[stories.forms.inputs]
[stories.layout.cards]))
(ns stories.buttons
(:require [io.factorhouse.storybook.core :as storybook]
[your-app.components.button :refer [button]]))
(defmethod storybook/story "Component/Buttons/Primary" [_]
{:component button
:stories {:Default {:args {:children ["Click me"]}}
:Large {:args {:size :lg :children ["Large button"]}}}})
The dispatch value represents the hierarchical path of your story and determines the file output location, and therefore your Storybook structure:
Each story multi-method compiles to a ES module that Storybook.js can understand. The build hooks defined in your shadow-cljs profile takes care of this compilation step.
- Use forward slashes (
/
) to separate hierarchy levels - Follow PascalCase or camelCase conventions for readability
Dispatch Value | Output File |
---|---|
"Component/Buttons/Primary" |
.storybook/js-out/component/buttons/primary_story.js |
"Component/Forms/Input/Text" |
.storybook/js-out/component/forms/input/text_story.js |
"Layout/Card/Basic" |
.storybook/js-out/layout/card/basic_story.js |
"Feedback/Alert/Types" |
.storybook/js-out/feedback/alert/types_story.js |
Popular Organizational Approaches:
-
Atomic Design - Groups by complexity level:
Atoms/Forms/SearchBox Molecules/Button/Primary Organisms/Navigation/Header Templates/Layout/Homepage
-
Functional Grouping - Groups by purpose (used by Spotify, Monday.com):
Component/Forms/Input Component/Navigation/Menu Component/Feedback/Alert
-
Feature-based - Groups by application area:
Dashboard/Charts/LineChart Profile/Settings/AccountForm Commerce/Product/Card
storybook/story
must return a map with the following structure:
{:component component-function ; Required: The component function to render (either a UIx or HSX component)
:stories story-map} ; Required: Map of story definitions
- Required: The actual component function reference
- Type: Function that accepts props and returns UIx/HSX component
- Required: A map where keys are story names and values are story configurations
- Type:
{StoryName StoryConfig}
{:args {} ; Required: All component props go here
:name "Story Name" ; Optional: Display name (defaults to map key)
:play play-fn ; Optional: Play function for interactions
:decorators [decorator] ; Optional: Story decorators
:parameters {} ; Optional: Story parameters}
{:args {:variant :primary ; Component prop
:size :lg ; Component prop
:disabled true ; Component prop
:children ["Button Text"]}} ; Component children
(defmethod storybook/story "Component/Buttons/Primary" [_]
{:component button
:stories {:Default {:args {:children ["Primary Button"]}}
:Large {:args {:size :lg :children ["Large Primary"]}}
:Disabled {:args {:disabled true :children ["Disabled"]}}
:WithClick {:args {:on-click #(js/alert "Clicked!")
:children ["Interactive"]}}}})
(defmethod storybook/story "Component/Overlay/Modal" [_]
{:component modal
:stories {:Confirmation {:args {:open true
:title "Confirm Action"
:children ["Are you sure?"]}}
:decorators [(fn [story]
; Wrap story with additional context
story)]
:parameters {:layout "fullscreen"}}
:Warning {:args {:open true
:title "Delete Item"
:children ["This cannot be undone"]
:variant :danger}}}})
StorybookJS expects story :args
as a plain JS object. In order to support rich Clojure types (keywords, sets, etc) storybook-cljs uses 'tagged JSON':
:foo ;; serializes to ["kw!", "foo"]
#{1 2 3} ;; serializes to ["set!", [1, 2, 3]]
Think of this tuple format like a reader literal in EDN.
This format is chosen over more complex formats (such as Transit+JSON) as it allows for easier viewing/editing of Clojure types within the Storybook controls UI:
See tagged_json.cljc to extend for custom types.
In the cases where Tagged JSON is too limiting for your components, simply wrap your story :component
with another function that handles your serdes logic.
If storybook-cljs does not support your React library of choice, you can always write a customer compiler.
See: compiler/uix.cljs for reference.
You will need to create a namespace which JS exports a function named storybook
.
Copyright © 2025 Factor House Pty Ltd.
Distributed under the Apache-2.0 License, the same as Apache Kafka.