Basic
The most basic example. The size of the floating layer depends on the contents region.
Code
<%= render Hakumi::Popover::Component.new(
title: "Title",
body: "This is the content of the popover.",
trigger: "click",
id: "popover-basic"
) do %>
<%= render(Hakumi::Button::Component.new(type: :primary)) { "Click me" } %>
<% end %>
Three ways to trigger
Mouse to click, focus and move in.
Code
<%= render Hakumi::Space::Component.new(size: :large, wrap: true) do |space| %>
<% space.with_item do %>
<%= render Hakumi::Popover::Component.new(
title: "Click",
body: "Trigger is click.",
trigger: "click"
) do %>
<%= render(Hakumi::Button::Component.new) { "Click" } %>
<% end %>
<% end %>
<% space.with_item do %>
<%= render Hakumi::Popover::Component.new(
title: "Focus",
body: "Trigger is focus.",
trigger: "focus"
) do %>
<%= render(Hakumi::Button::Component.new) { "Focus" } %>
<% end %>
<% end %>
<% space.with_item do %>
<%= render Hakumi::Popover::Component.new(
title: "Hover",
body: "Trigger is hover.",
trigger: "hover"
) do %>
<%= render(Hakumi::Button::Component.new) { "Hover" } %>
<% end %>
<% end %>
<% end %>
Placement
There are 12 placement options available.
Code
<div class="popover-placement-demo">
<%= render Hakumi::Flex::Component.new(vertical: true, align: :center, gap: :middle) do %>
<%= render Hakumi::Flex::Component.new(gap: :small, justify: :center) do %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "topLeft") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "TL" } %>
<% end %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "top") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "Top" } %>
<% end %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "topRight") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "TR" } %>
<% end %>
<% end %>
<%= render Hakumi::Flex::Component.new(justify: :center, gap: 280) do %>
<%= render Hakumi::Flex::Component.new(vertical: true, gap: :small) do %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "leftTop") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "LT" } %>
<% end %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "left") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "Left" } %>
<% end %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "leftBottom") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "LB" } %>
<% end %>
<% end %>
<%= render Hakumi::Flex::Component.new(vertical: true, gap: :small) do %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "rightTop") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "RT" } %>
<% end %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "right") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "Right" } %>
<% end %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "rightBottom") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "RB" } %>
<% end %>
<% end %>
<% end %>
<%= render Hakumi::Flex::Component.new(gap: :small, justify: :center) do %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "bottomLeft") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "BL" } %>
<% end %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "bottom") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "Bottom" } %>
<% end %>
<%= render Hakumi::Popover::Component.new(title: "Title", body: "Content", placement: "bottomRight") do %>
<%= render(Hakumi::Button::Component.new(class: "placement-btn")) { "BR" } %>
<% end %>
<% end %>
<% end %>
</div>
<style>
.popover-placement-demo {
padding: 100px 0;
width: 100%;
display: flex;
justify-content: center;
}
.placement-btn {
width: 80px;
justify-content: center;
}
</style>
Arrow
Hide arrow by arrow.
Code
<%= render Hakumi::Space::Component.new(size: :large) do |space| %>
<% space.with_item do %>
<%= render Hakumi::Popover::Component.new(
title: "With arrow",
body: "This popover shows the arrow.",
trigger: "click"
) do %>
<%= render(Hakumi::Button::Component.new) { "Arrow" } %>
<% end %>
<% end %>
<% space.with_item do %>
<%= render Hakumi::Popover::Component.new(
title: "No arrow",
body: "This popover hides the arrow.",
trigger: "click",
arrow: false
) do %>
<%= render(Hakumi::Button::Component.new) { "No Arrow" } %>
<% end %>
<% end %>
<% end %>
Controlling the close of the dialog
Use open prop to control the display of the card.
Code
<%= render Hakumi::Space::Component.new(size: :large, align: :center) do |space| %>
<% space.with_item do %>
<%= render Hakumi::Popover::Component.new(
title: "Controlled",
body: "This popover is controlled by the open prop.",
trigger: "click",
open: true,
id: "popover-controlled"
) do %>
<%= render(Hakumi::Button::Component.new) { "Controlled" } %>
<% end %>
<% end %>
<% space.with_item do %>
<%= render Hakumi::Space::Component.new(size: :small) do |buttons| %>
<% buttons.with_item do %>
<%= render(Hakumi::Button::Component.new(id: "popover-controlled-open")) { "Open" } %>
<% end %>
<% buttons.with_item do %>
<%= render(Hakumi::Button::Component.new(id: "popover-controlled-close")) { "Close" } %>
<% end %>
<% end %>
<% end %>
<% end %>
<script>
(() => {
const popover = document.getElementById("popover-controlled")
const openButton = document.getElementById("popover-controlled-open")
const closeButton = document.getElementById("popover-controlled-close")
if (!popover || !openButton || !closeButton) return
const wire = () => {
const api = popover.hakumiPopover
if (!api) return false
openButton.addEventListener("click", () => api.setOpen(true))
closeButton.addEventListener("click", () => api.setOpen(false))
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>
Auto Shift
Auto adjust Popup and arrow position when Popover is close to the edge of the screen. Will be out of screen when exceed limitation.
Code
<div class="popover-auto-shift-demo">
<%= render Hakumi::Flex::Component.new(justify: :end, align: :center, gap: :middle) do %>
<%= render Hakumi::Popover::Component.new(
title: "Auto shift",
body: "This popover should auto flip when near edges.",
placement: "right",
trigger: "click",
auto_adjust_overflow: true
) do %>
<%= render(Hakumi::Button::Component.new) { "Right Edge" } %>
<% end %>
<% end %>
</div>
<style>
.popover-auto-shift-demo {
padding: 24px;
height: 200px;
display: flex;
align-items: center;
justify-body: flex-end;
}
</style>
Hover with click popover
The following example shows how to create a popover which can be hovered and clicked.
Code
<%= render Hakumi::Popover::Component.new(
title: "Hover or click",
body: "This popover can be hovered and clicked.",
trigger: "hover,click"
) do %>
<%= render(Hakumi::Button::Component.new) { "Hover & Click" } %>
<% end %>
Programmatic
Render a popover with HakumiComponents.renderComponent.
Code
<%= render Hakumi::Space::Component.new(direction: :vertical, size: :middle) do |space| %>
<% space.with_item do %>
<%= render(Hakumi::Button::Component.new(id: "popover-programmatic-button")) { "Render Popover" } %>
<% end %>
<% space.with_item do %>
<div id="popover-programmatic-target"></div>
<% end %>
<% end %>
<script>
(() => {
const button = document.getElementById("popover-programmatic-button")
const target = document.getElementById("popover-programmatic-target")
if (!button || !target) return
const wire = () => {
if (!window.HakumiComponents?.renderComponent) return false
button.addEventListener("click", async () => {
await window.HakumiComponents.renderComponent("popover", {
target,
params: {
title: "Programmatic",
body: "Rendered via HakumiComponents.renderComponent.",
body: "Hover me",
trigger: "hover",
placement: "bottom"
}
})
})
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>
Popover Ruby Props
| Prop | Type | Default | Description |
|---|---|---|---|
title |
String or ViewComponent |
- |
Title of the popover. |
content |
String or ViewComponent |
- |
Content of the popover body. |
body |
String or ViewComponent |
- |
Fallback trigger content when no block is provided (useful for programmatic rendering). |
placement |
String |
"top" |
The position of the popover relative to the target. |
trigger |
String |
"hover" |
Popover trigger mode (hover, click, focus). Use comma-separated values for multiple triggers. |
open |
Boolean |
nil |
Whether the popover is visible (controlled mode). |
arrow |
Boolean |
true |
Whether to show the arrow. |
disabled |
Boolean |
false |
Whether to disable popover. |
auto_adjust_overflow |
Boolean |
true |
Whether to automatically adjust placement when overflowing. |
JavaScript API (element.hakumiPopover)
| Prop | Type | Default | Description |
|---|---|---|---|
open() |
Function |
- |
Show the popover manually. |
close() |
Function |
- |
Hide the popover manually. |
toggle() |
Function |
- |
Toggle the popover visibility. |
isOpen() |
Function |
- |
Returns true if the popover is visible. |
getState() |
Function |
- |
Returns { open, placement, trigger, arrow, disabled }. |
setPlacement(placement) |
Function |
- |
Change the placement dynamically. |
setOpen(value) |
Function |
- |
Set the open state in controlled mode. |