To start using PhoenixStorybook in your Phoenix application you will need to follow these steps:

  1. Add the phoenix_storybook dependency
  2. Create your storybook backend module
  3. Add storybook access to your router
  4. Make your components' assets available
  5. Update your Docker image
  6. 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"}
  ]
end

2. 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"
end

3. 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
end

4. 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"
    ]
  ]
end

5. 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 storybook

6. 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