Popover

Floating panel anchored to a trigger, with click, hover, focus, and programmatic open modes.

Basic

.heex
<.popover>
  <.button>
    <.icon name="hero-share" class="icon" /> Share
  </.button>

  <:content>
    <h3 class="text-base font-semibold text-foreground">Share your project</h3>
    <p class="mt-1 text-sm text-foreground-softer">
      Anyone with the link can view this project.
    </p>

    <div class="mt-3">
      <.input
        name="link"
        readonly
        value="https://fluxonui.com/p/atlas"
        label="Project link"
      >
        <:outer_suffix>
          <.button>
            <.icon name="hero-document-duplicate" class="icon" /> Copy
          </.button>
        </:outer_suffix>
      </.input>
    </div>
  </:content>
</.popover>

Placement

Pick the preferred side with placement. The panel flips to the opposite side when there is not enough room and shifts to stay inside the viewport.

.heex
<.popover placement="top">
  <.button variant="ghost">Top</.button>
  <:content>
    <p class="text-sm">Anchored above the trigger.</p>
  </:content>
</.popover>

<.popover placement="right">
  <.button variant="ghost">Right</.button>
  <:content>
    <p class="text-sm">Anchored beside the trigger.</p>
  </:content>
</.popover>

<.popover placement="bottom">
  <.button variant="ghost">Bottom</.button>
  <:content>
    <p class="text-sm">Anchored below the trigger.</p>
  </:content>
</.popover>

<.popover placement="left">
  <.button variant="ghost">Left</.button>
  <:content>
    <p class="text-sm">Anchored beside the trigger.</p>
  </:content>
</.popover>

Edge alignment

*-start aligns the panel's leading edge with the trigger; *-end aligns the trailing edges. Use these when the panel is wider than the trigger or sits near a layout edge.

.heex
<.popover placement="bottom-start" class="w-64">
  <.button variant="outline">Bottom start</.button>
  <:content>
    <p class="text-sm">Leading edges aligned. Panel extends to the right.</p>
  </:content>
</.popover>

<.popover placement="bottom-end" class="w-64">
  <.button variant="outline">Bottom end</.button>
  <:content>
    <p class="text-sm">Trailing edges aligned. Panel extends to the left.</p>
  </:content>
</.popover>

Open on hover

Set open_on_hover for glanceable, passive info bubbles. The panel stays open while the cursor is over it so links and controls inside remain reachable. Suppressed on touch devices, where click takes over.

.heex
<.popover open_on_hover placement="top" class="w-72 p-0">
  <button
    type="button"
    class="cursor-pointer size-9 rounded-full border border-base bg-surface flex items-center justify-center text-foreground hover:highlight transition-colors"
  >
    <svg viewBox="0 0 24 24" class="size-5 -rotate-90" fill="none">
      <circle
        cx="12"
        cy="12"
        r="9"
        stroke="currentColor"
        stroke-width="2.5"
        class="text-foreground-softest/40"
        stroke-linecap="round"
      />
      <circle
        cx="12"
        cy="12"
        r="9"
        stroke="currentColor"
        stroke-width="2.5"
        class="text-foreground"
        stroke-linecap="round"
        stroke-dasharray="13.6 56.6"
      />
    </svg>
  </button>

  <:content>
    <div class="p-4">
      <div class="flex items-center gap-2">
        <h3 class="text-base font-semibold text-foreground">Context window</h3>
        <span class="rounded-md bg-sunken px-1.5 py-0.5 text-xs font-medium text-foreground-soft tabular-nums">
          24%
        </span>
      </div>
      <p class="mt-1 text-sm text-foreground-softer tabular-nums">
        52k of 258k tokens used
      </p>
      <div class="mt-3 h-1.5 w-full overflow-hidden rounded-full bg-sunken">
        <div class="h-full w-[24%] rounded-full bg-foreground"></div>
      </div>
    </div>
    <div class="border-t border-base px-4 py-2.5">
      <p class="text-xs text-foreground-softer">
        Auto-compacts when the context fills up.
      </p>
    </div>
  </:content>
</.popover>

Open on focus

Set open_on_focus to attach the panel to an input. Click toggling and Escape are suppressed so the panel does not flip closed while the user is typing.

.heex
<.popover open_on_focus placement="bottom-start" class="w-80 p-0">
  <.input type="search" name="query" placeholder="Search users..." phx-debounce="300" />

  <:content>
    <div class="py-2">
      <button
        :for={user <- @users}
        type="button"
        class="w-full flex items-center gap-3 px-3 py-2 hover:highlight"
      >
        <img src={user.avatar} alt="" class="size-8 rounded-full ring-1 ring-black/5" />
        <div class="flex flex-col items-start leading-tight">
          <span class="text-sm font-medium">{user.name}</span>
          <span class="text-xs text-foreground-softer">{user.email}</span>
        </div>
      </button>
    </div>
  </:content>
</.popover>

Inline form help

Drop a popover inside an input's :inner_suffix slot for contextual help that does not steal focus from the field.

.heex
<.input name="api_key" label="API key" value="sk_test_4eC39HqLyjWDarjtT1zdp7dc">
  <:inner_suffix>
    <.popover open_on_hover placement="right">
      <.icon name="hero-question-mark-circle" class="size-4 text-foreground-softer" />

      <:content>
        <p class="text-sm font-medium text-foreground">About API keys</p>
        <p class="mt-1 text-sm text-foreground-softer max-w-xs">
          Used to authenticate server-side requests. Treat it like a password and never embed it in client code.
        </p>
      </:content>
    </.popover>
  </:inner_suffix>
</.input>

Notification feed

A scrollable list with a header and a footer action, anchored under a bell trigger. The panel becomes a small inbox without leaving the page.

.heex
<.popover placement="bottom-end" class="w-80 p-0">
  <button
    type="button"
    aria-label="Notifications"
    class="cursor-pointer relative size-9 rounded-full flex items-center justify-center text-foreground-softer hover:highlight"
  >
    <.icon name="hero-bell" class="size-5" />
    <span class="absolute top-2 right-2.5 size-2 rounded-full bg-primary ring-2 ring-surface"></span>
  </button>

  <:content>
    <div class="flex items-center justify-between px-3 py-2 border-b border-base">
      <span class="text-sm font-semibold">Notifications</span>
      <button type="button" class="text-xs text-foreground-softer hover:text-foreground">
        Mark all read
      </button>
    </div>

    <ul class="max-h-72 overflow-y-auto py-1">
      <li :for={n <- @notifications} class="flex items-start gap-3 px-3 py-2 hover:highlight">
        <img src={n.avatar} alt="" class="size-8 rounded-full ring-1 ring-black/5 mt-0.5" />
        <div class="min-w-0 flex-1">
          <p class="text-sm">
            <span class="font-medium">{n.actor}</span> {n.action}
          </p>
          <p class="text-xs text-foreground-softer tabular-nums mt-0.5">{n.time_ago}</p>
        </div>
      </li>
    </ul>

    <.link
      navigate={~p"/activity"}
      class="block border-t border-base px-3 py-2 text-center text-xs font-medium text-foreground-soft hover:highlight"
    >
      View all activity
    </.link>
  </:content>
</.popover>

Color picker

A grid of options inside the panel. Anything that fits a small canvas (colors, emoji, icons) maps onto this pattern. Click a swatch to update the trigger.

.heex
<.popover id="color-picker" placement="bottom-start" class="w-56">
  <button
    id="color-trigger"
    type="button"
    aria-label="Pick a color"
    class="cursor-pointer size-7 rounded-full ring-1 ring-black/10 hover:ring-black/20"
    style="background-color: #6366f1"
  ></button>

  <:content>
    <div class="grid grid-cols-5 gap-2">
      <button
        :for={color <- @colors}
        type="button"
        data-swatch
        data-active={color.value == @selected_color}
        aria-label={color.name}
        phx-click={
          JS.remove_attribute("data-active", to: "#color-picker [data-swatch]")
          |> JS.set_attribute({"data-active", "true"})
          |> JS.set_attribute({"style", "background-color: " <> color.hex}, to: "#color-trigger")
        }
        style={"background-color: " <> color.hex}
        class="size-7 rounded-full ring-1 ring-black/10 hover:ring-black/20 data-[active]:ring-2 data-[active]:ring-foreground data-[active]:ring-offset-2 data-[active]:ring-offset-surface"
      ></button>
    </div>
  </:content>
</.popover>

Rich interactive content

The :content slot accepts arbitrary HEEx, including switches, selects, and forms. Tab moves through interactive children without trapping focus inside the panel.

.heex
<.popover placement="bottom-end" class="w-64 p-0">
  <.button variant="ghost">
    <.icon name="hero-adjustments-horizontal" class="icon" /> Table settings
  </.button>

  <:content>
    <div class="p-3">
      <p class="text-[10px] font-semibold uppercase tracking-wider text-foreground-softer mb-1.5">
        Sort by
      </p>
      <.select
        native
        name="sort_by"
        value="Name"
        options={["Name", "Date modified", "Size", "Type"]}
        size="sm"
        class="w-full shadow-none"
      />
    </div>

    <div class="border-t border-base p-3">
      <p class="text-[10px] font-semibold uppercase tracking-wider text-foreground-softer mb-2">
        Visible columns
      </p>
      <div class="space-y-1">
        <label
          :for={col <- @columns}
          for={"col-" <> col.key}
          class="flex items-center justify-between gap-2 rounded-md px-1.5 py-1 cursor-pointer hover:highlight"
        >
          <span class="text-sm">{col.name}</span>
          <.switch id={"col-" <> col.key} name={"col_" <> col.key} checked={col.visible} />
        </label>
      </div>
    </div>
  </:content>
</.popover>

Programmatic control

Fluxon.open_popover/1 and Fluxon.close_popover/1 build JS commands that toggle a popover by id from anywhere, including elements outside the popover wrapper.

External controls
.heex
<div class="flex items-center gap-2 rounded-full border border-dashed border-base px-3 py-1.5">
  <span class="text-xs text-foreground-softer">External controls</span>
  <.button size="sm" variant="ghost" phx-click={Fluxon.open_popover("project-info")}>
    Open
  </.button>
  <.button size="sm" variant="ghost" phx-click={Fluxon.close_popover("project-info")}>
    Close
  </.button>
</div>

<.popover id="project-info" placement="bottom" class="w-72">
  <.button variant="outline">
    <.icon name="hero-information-circle" class="icon" /> Project info
  </.button>

  <:content>
    <h3 class="text-sm font-semibold">Atlas onboarding</h3>
    <p class="mt-1 text-sm text-foreground-softer">
      Customer onboarding flow for the Atlas product line.
    </p>
    <p class="mt-3 text-xs text-foreground-softer tabular-nums">
      Updated 12 minutes ago
    </p>
  </:content>
</.popover>

Multi-step workflow

Anchor several popovers to the same trigger via target and chain open/close JS commands to build a guided flow without server round trips.

.heex
<.button id="setup-trigger" phx-click={Fluxon.open_popover("setup-step-1")}>
  <.icon name="hero-sparkles" class="icon" /> Quick setup
</.button>

<.popover id="setup-step-1" target="#setup-trigger" placement="bottom" class="w-80 p-0">
  <:content>
    <div class="p-5">
      <div class="flex items-center gap-1">
        <span class="h-1 flex-1 rounded-full bg-foreground"></span>
        <span class="h-1 flex-1 rounded-full bg-foreground-softest/30"></span>
        <span class="h-1 flex-1 rounded-full bg-foreground-softest/30"></span>
      </div>
      <h3 class="mt-4 text-base font-semibold">Name your workspace</h3>
      <p class="mt-1 text-sm text-foreground-softer">
        This is what your team will see in the sidebar.
      </p>
      <div class="mt-4">
        <.input name="workspace" label="Workspace name" placeholder="Acme Inc." />
      </div>
    </div>
    <div class="grid grid-cols-3 items-center gap-2 border-t border-base px-3 py-2">
      <div></div>
      <span class="text-center text-xs text-foreground-softer tabular-nums">1 of 3</span>
      <div class="flex justify-end">
        <.button
          size="sm"
          variant="solid"
          color="primary"
          phx-click={Fluxon.close_popover("setup-step-1") |> Fluxon.open_popover("setup-step-2")}
        >
          Continue
        </.button>
      </div>
    </div>
  </:content>
</.popover>

<.popover id="setup-step-2" target="#setup-trigger" placement="bottom" class="w-80 p-0">
  <:content>
    <div class="p-5">
      <div class="flex items-center gap-1">
        <span class="h-1 flex-1 rounded-full bg-foreground"></span>
        <span class="h-1 flex-1 rounded-full bg-foreground"></span>
        <span class="h-1 flex-1 rounded-full bg-foreground-softest/30"></span>
      </div>
      <h3 class="mt-4 text-base font-semibold">Invite your team</h3>
      <p class="mt-1 text-sm text-foreground-softer">
        Add teammates by email. You can do this later from Settings.
      </p>
      <div class="mt-4">
        <.input
          name="invites"
          label="Email addresses"
          placeholder="ada@acme.com, grace@acme.com"
        />
      </div>
    </div>
    <div class="grid grid-cols-3 items-center gap-2 border-t border-base px-3 py-2">
      <div>
        <.button
          size="sm"
          variant="ghost"
          phx-click={Fluxon.close_popover("setup-step-2") |> Fluxon.open_popover("setup-step-1")}
        >
          Back
        </.button>
      </div>
      <span class="text-center text-xs text-foreground-softer tabular-nums">2 of 3</span>
      <div class="flex justify-end">
        <.button
          size="sm"
          variant="solid"
          color="primary"
          phx-click={Fluxon.close_popover("setup-step-2") |> Fluxon.open_popover("setup-step-3")}
        >
          Continue
        </.button>
      </div>
    </div>
  </:content>
</.popover>

<.popover id="setup-step-3" target="#setup-trigger" placement="bottom" class="w-80 p-0">
  <:content>
    <div class="p-5 text-center">
      <div class="flex items-center gap-1">
        <span class="h-1 flex-1 rounded-full bg-foreground"></span>
        <span class="h-1 flex-1 rounded-full bg-foreground"></span>
        <span class="h-1 flex-1 rounded-full bg-foreground"></span>
      </div>
      <div class="mt-5 inline-flex size-11 items-center justify-center rounded-full bg-success/10 text-success">
        <.icon name="hero-check" class="size-6" />
      </div>
      <h3 class="mt-3 text-base font-semibold">You are all set</h3>
      <p class="mt-1 text-sm text-foreground-softer">
        Your workspace is ready. Jump in and create your first project.
      </p>
    </div>
    <div class="grid grid-cols-3 items-center gap-2 border-t border-base px-3 py-2">
      <div>
        <.button
          size="sm"
          variant="ghost"
          phx-click={Fluxon.close_popover("setup-step-3") |> Fluxon.open_popover("setup-step-2")}
        >
          Back
        </.button>
      </div>
      <span class="text-center text-xs text-foreground-softer tabular-nums">3 of 3</span>
      <div class="flex justify-end">
        <.button
          size="sm"
          variant="solid"
          color="primary"
          phx-click={Fluxon.close_popover("setup-step-3")}
        >
          Finish
        </.button>
      </div>
    </div>
  </:content>
</.popover>

Guided tour

Point each step's target at a different element on the page to walk the user through real UI without an overlay. Each popover is opened by chaining open/close JS commands from the previous step.

A Atlas
.heex
<header class="flex items-center justify-between rounded-base border border-base px-3 py-2">
  <div class="flex items-center gap-2">
    <span class="size-7 rounded-md bg-indigo-500 text-white text-sm font-semibold flex items-center justify-center">
      A
    </span>
    <span class="text-sm font-medium">Atlas</span>
  </div>
  <div class="flex items-center gap-1">
    <button
      id="tour-search"
      type="button"
      class="size-9 rounded-md flex items-center justify-center hover:highlight"
      aria-label="Search"
    >
      <.icon name="hero-magnifying-glass" class="size-5" />
    </button>
    <button
      id="tour-bell"
      type="button"
      class="size-9 rounded-md flex items-center justify-center hover:highlight"
      aria-label="Notifications"
    >
      <.icon name="hero-bell" class="size-5" />
    </button>
    <button
      id="tour-avatar"
      type="button"
      class="size-7 rounded-full ring-1 ring-black/5 overflow-hidden"
      aria-label="Account"
    >
      <img src={@current_user.avatar} alt="" class="size-full" />
    </button>
  </div>
</header>

<.button phx-click={Fluxon.open_popover("tour-1")}>
  <.icon name="hero-sparkles" class="icon" /> Take the tour
</.button>

<.popover id="tour-1" target="#tour-search" placement="bottom" class="w-72 p-0">
  <:content>
    <.tour_card
      step={1}
      title="Search anywhere"
      body="Press Cmd K from any page to jump to a file or recent action."
      next="tour-2"
    />
  </:content>
</.popover>

<.popover id="tour-2" target="#tour-bell" placement="bottom" class="w-72 p-0">
  <:content>
    <.tour_card
      step={2}
      title="Stay on top of activity"
      body="Mentions and updates land here. We will only ping you for things that need a response."
      next="tour-3"
      previous="tour-1"
    />
  </:content>
</.popover>

<.popover id="tour-3" target="#tour-avatar" placement="bottom-end" class="w-72 p-0">
  <:content>
    <.tour_card
      step={3}
      title="Manage your account"
      body="Switch workspaces, change your theme, or sign out from the avatar menu."
      previous="tour-2"
    />
  </:content>
</.popover>

Custom styling

Pass class to override width, padding, or surface color on the panel. Use p-0 when the content draws its own sections.

.heex
<.popover placement="top-end" class="w-80 p-0 shadow-lg bg-zinc-50">
  <button class="cursor-pointer size-10 flex items-center justify-center border border-zinc-200 rounded-full shadow-sm">
    <span class="text-zinc-500 font-semibold">?</span>
  </button>

  <:content>
    <div class="p-4">
      <h3 class="font-semibold text-zinc-700">Help</h3>
      <p class="mt-1 text-sm text-zinc-600">Get help and learn about our platform.</p>
    </div>

    <div class="border border-zinc-200 rounded-base p-4 -m-px bg-white">
      <h4 class="font-semibold text-sm text-zinc-700">Quick links</h4>
      <ul class="mt-3 space-y-2 text-sm">
        <li>
          <a href="#" class="inline-flex items-center gap-2 text-zinc-600 hover:text-zinc-900">
            <.icon name="hero-question-mark-circle" class="size-4" /> Help Center
          </a>
        </li>
        <li>
          <a href="#" class="inline-flex items-center gap-2 text-zinc-600 hover:text-zinc-900">
            <.icon name="hero-book-open" class="size-4" /> Documentation
          </a>
        </li>
        <li>
          <a href="#" class="inline-flex items-center gap-2 text-zinc-600 hover:text-zinc-900">
            <.icon name="hero-sparkles" class="size-4" /> What's new
          </a>
        </li>
      </ul>
    </div>

    <p class="px-4 py-2 text-sm text-zinc-500">
      Still stuck? <a href="#" class="underline">Contact support</a>.
    </p>
  </:content>
</.popover>