To start using PhoenixStorybook in your Phoenix application you will need to follow these steps:
- Add the
phoenix_storybookdependency - Create your storybook backend module
- Add storybook access to your router
- Make your components' assets available
- Update your Docker image
- Create some content
1. Add the phoenix_storybook dependency
Add the following to your mix.exs and run mix deps.get:
def deps do
[
{:phoenix_storybook, "~> 1.2.0"}
]
end2. Create your storybook backend module
Create a new module under your application lib folder:
# lib/my_app_web/storybook.ex
defmodule MyAppWeb.Storybook do
use PhoenixStorybook,
otp_app: :my_app,
content_path: Path.expand("../../storybook", __DIR__),
# assets path are remote path, not local file-system paths
css_path: "/assets/css/storybook.css",
js_path: "/assets/js/storybook.js",
sandbox_class: "my-app"
end3. Add storybook access to your router
Once installed, update your router's configuration to forward requests to a PhoenixStorybook
with a unique name of your choice:
# lib/my_app_web/router.ex
use MyAppWeb, :router
import PhoenixStorybook.Router
...
scope "/" do
storybook_assets()
end
scope "/", MyAppWeb do
pipe_through :browser
...
live_storybook "/storybook", backend_module: MyAppWeb.Storybook
end4. Make your components' assets available
PhoenixStorybook loads the css_path / js_path bundles you configured above — not your
application's app.css / app.js. You need to build and serve those two bundles. The steps below
assume a default Phoenix 1.8 app (Tailwind v4 + esbuild); adjust the paths if your asset pipeline
differs. Sub-steps b, d and e are Tailwind-specific — on another pipeline, substitute your own CSS
build, watcher, and deploy steps. This is exactly what mix phx.gen.storybook walks you through.
a. JS bundle
This script is loaded immediately before PhoenixStorybook's own JS. Use it to declare your LiveView
Hooks, Params and Uploaders on window.storybook — keep only the ones your components need:
// assets/js/storybook.js
import * as Hooks from "./hooks";
import * as Params from "./params";
import * as Uploaders from "./uploaders";
(function () {
window.storybook = { Hooks, Params, Uploaders };
})();Add it as a new entry point to your existing esbuild profile in config/config.exs:
config :esbuild,
my_app: [
args:
~w(js/app.js js/storybook.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/* --alias:@=.),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => [Path.expand("../deps", __DIR__), Mix.Project.build_path()]}
]b. CSS bundle
Create assets/css/storybook.css. Because PhoenixStorybook loads this file instead of your
app.css, you must mirror any @plugin, theme, custom variant or font your components rely on —
otherwise they render unstyled:
/* assets/css/storybook.css */
@import "tailwindcss" source(none);
@source "../css";
@source "../js";
@source "../../lib/my_app_web";
@source "../../storybook";
/* Mirror here any @plugin / @custom-variant / theme blocks from your app.css */Add a storybook Tailwind build profile in config/config.exs:
config :tailwind,
my_app: [
...
],
storybook: [
args: ~w(
--input=assets/css/storybook.css
--output=priv/static/assets/css/storybook.css
),
cd: Path.expand("..", __DIR__)
]c. Scope your styles to the sandbox
All storybook containers carry your sandbox_class. Add it to your application layout body, and
nest your component styling under it so your app and the storybook stay in sync:
<!-- lib/my_app_web/components/layouts/root.html.heex -->
<body class="my-app">Optionally, nest your own scoped component styles under that class in assets/css/storybook.css.
Global @plugin / @custom-variant / theme directives (e.g. daisyUI) must stay at the top level —
only your bespoke component CSS goes under the sandbox class:
.my-app {
/* your custom component styling, e.g. */
h1 {
@apply text-2xl font-bold;
}
}ℹ️ Learn more on this topic in the sandboxing guide.
d. Dev watcher & live reload
In config/dev.exs, add a watcher so the storybook CSS rebuilds on change, and a live-reload
pattern for your stories:
config :my_app, MyAppWeb.Endpoint,
watchers: [
...
storybook_tailwind: {Tailwind, :install_and_run, [:storybook, ~w(--watch)]}
],
live_reload: [
patterns: [
...
~r"storybook/.*\.exs$"
]
]e. Formatter & build aliases
Add your stories to .formatter.exs (importing :phoenix_storybook keeps the storybook DSL paren-free):
[
import_deps: [..., :phoenix_storybook],
inputs: [
...
"storybook/**/*.exs"
]
]And make sure the storybook bundle is built with your other assets in mix.exs:
defp aliases do
[
...,
"assets.build": [
...
"tailwind storybook"
],
"assets.deploy": [
...
"tailwind storybook --minify",
"phx.digest"
]
]
end5. Update your Docker image
If you are deploying your app with Docker, then you need to copy the storybook content into your Docker image.
Add this to your Dockerfile:
COPY storybook storybook6. Create some content
Then you can start creating some content for your storybook. Storybook can contain different kinds of stories:
- component stories: to document and showcase your components across different variations.
- pages: to publish some UI guidelines, framework with regular HTML content.
- examples: to show how your components can be used and mixed in real UI pages.
Stories are described as Elixir scripts (.story.exs) created under your :content_path folder.
Feel free to organize them in sub-folders, as the hierarchy will be respected in your storybook
sidebar.
Here is an example of a stateless (function) component story:
# storybook/components/button.story.exs
defmodule MyAppWeb.Storybook.Components.Button do
alias MyAppWeb.Components.Button
# :live_component or :page are also available
use PhoenixStorybook.Story, :component
def function, do: &Button.button/1
def variations do [
%Variation{
id: :default,
attributes: %{
label: "A button"
}
},
%Variation{
id: :green_button,
attributes: %{
label: "Still a button",
color: :green
}
}
]
end
end