Drawer

Component

Interactive examples and API documentation

Basic Drawer
The most basic drawer usage.
Code
<%= render Hakumi::Button::Component.new(type: :primary, id: "basic-drawer-trigger") do %>
  Open Drawer
<% end %>

<%= render Hakumi::Drawer::Component.new(
  id: "basic-drawer",
  title: "Basic Drawer",
  open: false
) do %>
  <p>Some contents...</p>
  <p>Some contents...</p>
  <p>Some contents...</p>
<% end %>

<script>
  (() => {
    const button = document.getElementById("basic-drawer-trigger")
    const drawer = document.getElementById("basic-drawer")
    if (!button || !drawer) return

    const wire = () => {
      const api = drawer.hakumiComponent?.api
      if (!api) return false

      button.addEventListener("click", () => api.open())
      return true
    }

    if (wire()) return

    const onRegister = ({ detail }) => {
      if (detail.id !== drawer.id) return
      if (wire()) window.removeEventListener("hakumi-component:registered", onRegister)
    }

    window.addEventListener("hakumi-component:registered", onRegister)
  })()
</script>
Placements
Open drawers from different edges.
Code
<%= render Hakumi::Space::Component.new(wrap: true) do %>
  <%= render(Hakumi::Button::Component.new(id: "drawer-left-trigger")) { "Left" } %>
  <%= render(Hakumi::Button::Component.new(id: "drawer-right-trigger")) { "Right" } %>
  <%= render(Hakumi::Button::Component.new(id: "drawer-top-trigger")) { "Top" } %>
  <%= render(Hakumi::Button::Component.new(id: "drawer-bottom-trigger")) { "Bottom" } %>
<% end %>

<%= render Hakumi::Drawer::Component.new(
  id: "drawer-left",
  title: "Left Drawer",
  placement: :left
) do %>
  <p>Drawer content for left placement.</p>
<% end %>

<%= render Hakumi::Drawer::Component.new(
  id: "drawer-right",
  title: "Right Drawer",
  placement: :right
) do %>
  <p>Drawer content for right placement.</p>
<% end %>

<%= render Hakumi::Drawer::Component.new(
  id: "drawer-top",
  title: "Top Drawer",
  placement: :top,
  height: 240
) do %>
  <p>Drawer content for top placement.</p>
<% end %>

<%= render Hakumi::Drawer::Component.new(
  id: "drawer-bottom",
  title: "Bottom Drawer",
  placement: :bottom,
  height: 240
) do %>
  <p>Drawer content for bottom placement.</p>
<% end %>

<script>
  (() => {
    const mapping = [
      ["drawer-left-trigger", "drawer-left"],
      ["drawer-right-trigger", "drawer-right"],
      ["drawer-top-trigger", "drawer-top"],
      ["drawer-bottom-trigger", "drawer-bottom"]
    ]

    const wire = () => {
      let wired = true

      mapping.forEach(([buttonId, drawerId]) => {
        const button = document.getElementById(buttonId)
        const drawer = document.getElementById(drawerId)
        const api = drawer?.hakumiComponent?.api

        if (!button || !drawer || !api) {
          wired = false
          return
        }

        button.addEventListener("click", () => api.open())
      })

      return wired
    }

    if (wire()) return

    const onRegister = ({ detail }) => {
      if (!mapping.some(([, drawerId]) => drawerId === detail.id)) return
      if (wire()) window.removeEventListener("hakumi-component:registered", onRegister)
    }

    window.addEventListener("hakumi-component:registered", onRegister)
  })()
</script>
Custom Footer
Drawer with extra header actions and custom footer.
Code
<%= render Hakumi::Button::Component.new(type: :primary, id: "drawer-footer-trigger") do %>
  Open Drawer
<% end %>

<%= render Hakumi::Drawer::Component.new(
  id: "drawer-footer",
  title: "Drawer with Footer",
  extra: render(Hakumi::Button::Component.new(type: :text)) { "Extra Action" },
  footer: render(Hakumi::Space::Component.new) {
    safe_join([
      render(Hakumi::Button::Component.new) { "Cancel" },
      render(Hakumi::Button::Component.new(type: :primary)) { "Submit" }
    ])
  }
) do %>
  <p>Drawer content with custom footer.</p>
<% end %>

<script>
  (() => {
    const button = document.getElementById("drawer-footer-trigger")
    const drawer = document.getElementById("drawer-footer")
    if (!button || !drawer) return

    const wire = () => {
      const api = drawer.hakumiComponent?.api
      if (!api) return false

      button.addEventListener("click", () => api.open())
      return true
    }

    if (wire()) return

    const onRegister = ({ detail }) => {
      if (detail.id !== drawer.id) return
      if (wire()) window.removeEventListener("hakumi-component:registered", onRegister)
    }

    window.addEventListener("hakumi-component:registered", onRegister)
  })()
</script>
No Mask
Disable the backdrop mask.
Code
<%= render Hakumi::Button::Component.new(type: :primary, id: "drawer-no-mask-trigger") do %>
  Open Drawer
<% end %>

<%= render Hakumi::Drawer::Component.new(
  id: "drawer-no-mask",
  title: "Drawer without Mask",
  mask: false
) do %>
  <p>Drawer without a backdrop mask.</p>
<% end %>

<script>
  (() => {
    const button = document.getElementById("drawer-no-mask-trigger")
    const drawer = document.getElementById("drawer-no-mask")
    if (!button || !drawer) return

    const wire = () => {
      const api = drawer.hakumiComponent?.api
      if (!api) return false

      button.addEventListener("click", () => api.open())
      return true
    }

    if (wire()) return

    const onRegister = ({ detail }) => {
      if (detail.id !== drawer.id) return
      if (wire()) window.removeEventListener("hakumi-component:registered", onRegister)
    }

    window.addEventListener("hakumi-component:registered", onRegister)
  })()
</script>
Programmatic Drawer
Render a drawer via the component API endpoint and inject it into the page.
Code
<div class="hakumi-space hakumi-space-horizontal" style="gap: 12px;">
  <%= render(Hakumi::Button::Component.new(type: :primary, id: "programmatic-drawer-btn")) { "Render Drawer via API" } %>
  <div id="programmatic-drawer-target"></div>
</div>

<script>
  (() => {
    const button = document.getElementById("programmatic-drawer-btn")
    if (!button) return

    const wire = () => {
      if (!window.HakumiComponents?.renderComponent) return false

      button.addEventListener("click", async () => {
        const result = await window.HakumiComponents.renderComponent("drawer", {
          params: { title: "Programmatic Drawer", message: "Loaded from /hakumi/components/drawer", open: true },
          target: "#programmatic-drawer-target",
          mode: "destroy_on_close"
        })

        result?.element?.hakumiComponent.api?.open()
      })

      return true
    }

    if (wire()) return

    const onReady = () => {
      if (wire()) {
        document.removeEventListener("turbo:load", onReady)
        window.removeEventListener("load", onReady)
      }
    }

    document.addEventListener("turbo:load", onReady)
    window.addEventListener("load", onReady)
  })()
</script>

Drawer API

Prop Type Default Description
open Boolean false Controls visibility.
title String or ViewComponent - Drawer header title.
placement Symbol :right Drawer placement (left/right/top/bottom).
size Symbol :default Width/height preset (:default or :large).
width Integer or String - Custom width when placement is left/right.
height Integer or String - Custom height when placement is top/bottom.
closable Boolean true Show close button.
mask Boolean true Render backdrop.
mask_closable Boolean true Allow closing by clicking the mask.
keyboard Boolean true Close on ESC key.
footer ViewComponent nil Custom footer content.
extra ViewComponent nil Extra header content (actions).
destroy_on_close Boolean false Unmount body content after closing.
content slot Slot - Drawer body content.
**html_attributes Keyword args - Attributes merged into the root `.hakumi-drawer` (pass as kwargs such as `class:`, `style:`).

Drawer JavaScript API

Prop Type Default Description
open() Function - Open the drawer.
close() Function - Close the drawer.
toggle() Function - Toggle visibility.
isOpen() Function - Return current open state.
getState() Function - Return the current state and configuration.