9 overlay components — modal dialogs, side drawers, sheets, dropdown menus, context menus, popovers, tooltips, hover cards, and the command menu primitive.

Module: PhiaUi.Components.Overlay

import PhiaUi.Components.Overlay

All overlays use Phoenix.LiveView.JS for open/close with focus management. No extra hooks needed except where noted.


Table of Contents


dialog

Modal dialog with focus trap, Escape to close, and scroll lock.

Sub-components: dialog_trigger, dialog_content, dialog_header, dialog_title, dialog_description, dialog_footer, dialog_close

<%!-- Trigger + dialog --%>
<.dialog id="edit-user">
  <:trigger>
    <.button variant="outline">Edit user</.button>
  </:trigger>

  <:content>
    <.dialog_header>
      <.dialog_title>Edit user</.dialog_title>
      <.dialog_description>Update the user's profile information.</.dialog_description>
    </.dialog_header>

    <.form for={@form} phx-submit="update_user" class="space-y-4 mt-4">
      <.phia_input field={@form[:name]} label="Name" />
      <.phia_input field={@form[:email]} type="email" label="Email" />

      <.dialog_footer>
        <.dialog_close><.button variant="outline">Cancel</.button></.dialog_close>
        <.button type="submit">Save changes</.button>
      </.dialog_footer>
    </.form>
  </:content>
</.dialog>
<%!-- Server-controlled dialog --%>
<.button phx-click={JS.show(to: "#confirm-dialog")}>Delete</.button>

<.dialog id="confirm-dialog">
  <:content>
    <.dialog_header>
      <.dialog_title>Confirm deletion</.dialog_title>
    </.dialog_header>
    <p class="text-muted-foreground">This action cannot be undone.</p>
    <.dialog_footer class="mt-4">
      <.button variant="outline" phx-click={JS.hide(to: "#confirm-dialog")}>Cancel</.button>
      <.button variant="destructive" phx-click="confirm_delete">Delete</.button>
    </.dialog_footer>
  </:content>
</.dialog>

drawer

Full-height side panel that slides in from left or right.

<.drawer id="user-details" side={:right}>
  <:trigger>
    <.button variant="ghost" size="sm">View details</.button>
  </:trigger>

  <:content>
    <div class="p-6 h-full overflow-y-auto">
      <h2 class="text-lg font-semibold"><%= @user.name %></h2>
      <p class="text-muted-foreground"><%= @user.email %></p>
      <.separator class="my-6" />
      <.description_list>
        <:item label="Role"><.badge><%= @user.role %></.badge></:item>
        <:item label="Joined"><.relative_time datetime={@user.inserted_at} /></:item>
        <:item label="Status"><.status_indicator status={if @user.active, do: :online, else: :offline} /></:item>
      </.description_list>
    </div>
  </:content>
</.drawer>

Attrs: id (required), side (:left | :right, default :right), size (:sm | :md | :lg | :full)


sheet

Smaller bottom or side sheet. Ideal for mobile overlays or quick actions.

<.sheet id="quick-add" side={:bottom}>
  <:trigger>
    <.button><.icon name="plus" class="mr-2" />New task</.button>
  </:trigger>

  <:content>
    <div class="p-4 space-y-3">
      <h3 class="font-semibold">New task</h3>
      <.input name="title" placeholder="Task title…" />
      <.button class="w-full" phx-click="add_task">Add</.button>
    </div>
  </:content>
</.sheet>

Attrs: side (:top | :right | :bottom | :left)


Dropdown with items, submenus, separators, and labels.

<.dropdown_menu id="actions-dd">
  <:trigger>
    <.button variant="ghost" size="icon" aria-label="More options">
      <.icon name="more-horizontal" />
    </.button>
  </:trigger>

  <:content>
    <.dropdown_menu_item phx-click="edit" phx-value-id={@record.id}>
      <.icon name="pencil" size="sm" class="mr-2" />Edit
    </.dropdown_menu_item>
    <.dropdown_menu_item phx-click="duplicate">
      <.icon name="copy" size="sm" class="mr-2" />Duplicate
    </.dropdown_menu_item>
    <.dropdown_menu_separator />
    <.dropdown_menu_item variant="destructive" phx-click="delete" phx-value-id={@record.id}>
      <.icon name="trash" size="sm" class="mr-2" />Delete
    </.dropdown_menu_item>
  </:content>
</.dropdown_menu>

context_menu

Right-click context menu.

<.context_menu id="row-ctx">
  <:trigger>
    <div class="p-3 hover:bg-muted/50 rounded cursor-context-menu">
      Right-click me
    </div>
  </:trigger>

  <:content>
    <.context_menu_item phx-click="open">Open</.context_menu_item>
    <.context_menu_item phx-click="rename">Rename</.context_menu_item>
    <.context_menu_separator />
    <.context_menu_item variant="destructive" phx-click="delete">Delete</.context_menu_item>
  </:content>
</.context_menu>

popover

Non-modal popup anchored to a trigger — for forms, colour pickers, or rich content.

<.popover id="date-picker-pop">
  <:trigger>
    <.button variant="outline" class="gap-2">
      <.icon name="calendar" size="sm" />
      <%= if @date, do: Calendar.strftime(@date, "%b %d, %Y"), else: "Pick a date" %>
    </.button>
  </:trigger>

  <:content>
    <.calendar id="pop-calendar" selected={@date} on_select="set_date" />
  </:content>
</.popover>

tooltip

Hover tooltip anchored to any element.

<.tooltip content="Copy to clipboard">
  <.button variant="ghost" size="icon" phx-click="copy">
    <.icon name="copy" />
  </.button>
</.tooltip>

<%!-- Rich tooltip with side --%>
<.tooltip side={:right}>
  <:content>
    <p class="font-medium">Pro plan</p>
    <p class="text-xs text-muted-foreground">Unlimited seats, priority support</p>
  </:content>
  <.button variant="outline">Upgrade</.button>
</.tooltip>

Attrs: content (string shorthand), side (:top | :right | :bottom | :left), delay (ms)


hover_card

Larger hover card — show preview content on mouse-over.

<.hover_card id="user-hc">
  <:trigger>
    <a href={~p"/users/#{@user}"} class="font-medium hover:underline"><%= @user.name %></a>
  </:trigger>

  <:content>
    <div class="flex gap-3 p-1">
      <.avatar size="md">
        <.avatar_image src={@user.avatar_url} />
        <.avatar_fallback name={@user.name} />
      </.avatar>
      <div>
        <p class="font-semibold"><%= @user.name %></p>
        <p class="text-sm text-muted-foreground"><%= @user.bio %></p>
        <div class="flex gap-2 mt-2">
          <.badge variant="secondary"><%= @user.role %></.badge>
          <.relative_time datetime={@user.inserted_at} class="text-xs text-muted-foreground" />
        </div>
      </div>
    </div>
  </:content>
</.hover_card>

command

Low-level command menu primitive — used to build command_palette. Use this for custom command UIs embedded within pages.

<.command id="inline-cmd">
  <.command_input placeholder="Type a command…" />
  <.command_list>
    <.command_group label="Suggestions">
      <.command_item phx-click="new_file">
        <.icon name="file-plus" class="mr-2" />New file
      </.command_item>
      <.command_item phx-click="new_folder">
        <.icon name="folder-plus" class="mr-2" />New folder
      </.command_item>
    </.command_group>
  </.command_list>
</.command>

For the full keyboard-accessible command palette (⌘K), use command_palette from the Navigation module.