Popover

Component

Interactive examples and API documentation

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.