Skip to content

Lesson 8 — Lookbook previews for interactive components

Lookbook renders previews inside an iframe that loads your full application layout — including JavaScript. With javascript_importmap_tags in the preview layout, Stimulus controllers connect automatically when the preview loads. Every interactive component built in this module works in Lookbook exactly as it does in the app.

Check your preview layout

Open app/views/layouts/lookbook/preview.html.erb and confirm it includes javascript_importmap_tags:

<!DOCTYPE html>
<html>
  <head>
    <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>
  <body class="p-8 bg-surface text-text">
    <%= yield %>
  </body>
</html>

If that tag is missing, add it now. Without it the Stimulus controllers won’t connect and interactive components will render but not respond.

Alert previews

The dismissible scenario tests the full dismiss flow — click the button and the alert fades out and is removed from the DOM.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# test/components/previews/alert_preview.rb
class AlertPreview < Lookbook::Preview
  layout "lookbook/preview"

  def info
    render Components::Alert.new(message: "This is an info message.", variant: :info)
  end

  def success
    render Components::Alert.new(message: "Operation successful.", variant: :success)
  end

  def warning
    render Components::Alert.new(message: "Please review your input.", variant: :warning)
  end

  def danger
    render Components::Alert.new(message: "Something went wrong.", variant: :danger)
  end

  def dismissible
    render Components::Alert.new(
      message:     "Click the × to dismiss this alert.",
      variant:     :info,
      dismissible: true
    )
  end
end

Modal previews

The modal renders open by default in Lookbook — passing open: true means you see the modal immediately without needing a trigger button. Test backdrop click, the × button, and the Escape key.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# test/components/previews/modal_preview.rb
class ModalPreview < Lookbook::Preview
  layout "lookbook/preview"

  def default
    render Components::Modal.new(title: "Example Modal", open: true) { |m|
      m.body do
        p { "Modal body content goes here." }
      end
    }
  end

  def with_footer
    render Components::Modal.new(title: "Confirm Action", open: true) { |m|
      m.body do
        p { "Are you sure you want to continue? This cannot be undone." }
      end
      m.footer do
        div(class: "flex gap-3 justify-end") do
          render Components::Button.new(label: "Cancel",  variant: :outline, type: "button",
                                        data: { action: "click->modal#close" })
          render Components::Button.new(label: "Confirm", variant: :danger,  type: "button")
        end
      end
    }
  end
end

Note that inside Lookbook preview classes you must use render Components::Button.new(...) rather than the Kit shorthand Button(...) — preview classes inherit from Lookbook::Preview, not Components::Base, so Kit methods are not available.

Dropdown previews

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# test/components/previews/dropdown_preview.rb
class DropdownPreview < Lookbook::Preview
  layout "lookbook/preview"

  def default
    render Components::Dropdown.new(label: "Actions") { |d|
      d.item "Edit",   url: "#"
      d.item "Archive", url: "#"
      d.item "Delete", url: "#"
    }
  end

  def align_right
    render Components::Dropdown.new(label: "Actions", align: :right) { |d|
      d.item "Edit",   url: "#"
      d.item "Archive", url: "#"
      d.item "Delete", url: "#"
    }
  end
end

Test click-outside close by clicking anywhere in the preview pane outside the menu. Test arrow key navigation by opening the menu and pressing ↑ and ↓. Test Escape to close.

Toast previews

duration: 0 disables auto-dismiss so the toast stays visible in Lookbook. Without it the toast disappears after four seconds — usually before you have finished inspecting it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# test/components/previews/toast_preview.rb
class ToastPreview < Lookbook::Preview
  layout "lookbook/preview"

  def info
    render Components::Toast.new(message: "Here is some information.", variant: :info, duration: 0)
  end

  def success
    render Components::Toast.new(message: "Board created successfully.", variant: :success, duration: 0)
  end

  def warning
    render Components::Toast.new(message: "Your session expires soon.", variant: :warning, duration: 0)
  end

  def danger
    render Components::Toast.new(message: "Something went wrong.", variant: :danger, duration: 0)
  end

  def auto_dismiss
    render Components::Toast.new(
      message:  "This toast dismisses after 4 seconds.",
      variant:  :info,
      duration: 4000
    )
  end
end

The auto_dismiss scenario is the one exception — it uses the real duration so you can observe the fade-out animation. Reload the preview to see it again.

Accordion previews

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# test/components/previews/accordion_preview.rb
class AccordionPreview < Lookbook::Preview
  layout "lookbook/preview"

  def single_expand
    render Components::Accordion.new { |a|
      a.panel(title: "What is KanbanFlow?", open: true) do
        p { "A multi-user Kanban board built with Phlex and Rails 8." }
      end
      a.panel(title: "How do I create a board?") do
        p { "Click '+ New Board' from the boards index." }
      end
      a.panel(title: "Can I invite team members?") do
        p { "Yes — board membership is covered in Module 11." }
      end
    }
  end

  def multiple_expand
    render Components::Accordion.new(multiple: true) { |a|
      a.panel(title: "First panel", open: true) do
        p { "Multiple panels can be open at the same time." }
      end
      a.panel(title: "Second panel", open: true) do
        p { "Both of these start open." }
      end
      a.panel(title: "Third panel") do
        p { "This one starts closed." }
      end
    }
  end
end

The single_expand scenario verifies that opening one panel closes the others. The multiple_expand scenario verifies that multiple panels can be open simultaneously.

Tabs previews

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# test/components/previews/tabs_preview.rb
class TabsPreview < Lookbook::Preview
  layout "lookbook/preview"

  def default
    render Components::Tabs.new { |t|
      t.tab "Overview" do
        p { "Board overview content." }
      end
      t.tab "Members" do
        p { "Member list content." }
      end
      t.tab "Settings" do
        p { "Board settings content." }
      end
    }
  end
end

Module 8 summary

  • Stimulus enhances server-rendered HTML — controllers attach via data-controller, read configuration from data-* attributes, and disconnect automatically when elements are removed from the DOM
  • In Phlex, data-* attributes are written explicitly in the component — clear, readable, and co-located with the HTML they describe. eagerLoadControllersFrom registers controllers automatically — no manual imports needed
  • Alert — properly wired dismiss with CSS transition
  • Modal — focus trap, keyboard escape, backdrop click to close, ARIA dialog role
  • Dropdown — click-outside close, arrow key navigation, full keyboard accessibility
  • Toast — queued ephemeral notifications, auto-dismiss, accessible live region, Turbo Stream integration, replaces flash messages
  • Accordion — single and multi-expand, CSS transitions, keyboard accessible
  • Tabs — server-rendered tab navigation, full ARIA tab pattern
  • Cross-controller communication — three patterns: outlets for tight coupling, custom events for loose coupling, common ancestor for coordinating multiple children

Components built this module

  • Components::Modal
  • Components::Dropdown
  • Components::ToastContainer
  • Components::Toast
  • Components::Accordion
  • Components::Tabs

Stimulus controllers written this module

  • alert_controller.js
  • modal_controller.js
  • dropdown_controller.js
  • toast_controller.js
  • accordion_controller.js
  • tabs_controller.js