Basic Drawer
The most basic drawer usage.
Basic Drawer
Some contents...
Some contents...
Some contents...
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.
Left Drawer
Drawer content for left placement.
Right Drawer
Drawer content for right placement.
Top Drawer
Drawer content for top placement.
Bottom Drawer
Drawer content for bottom placement.
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.
Drawer without Mask
Drawer without a 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. |