Modal

New

Overview

A centered modal dialog for focused forms, confirmations, and detail views, with native focus management and LiveView control.

Modal uses the native HTML dialog element for focus trapping, background inertness, top-layer rendering, and focus restoration. Use Modal for centered, focused work and Drawer for panels attached to a viewport edge.

The DizzyModal hook handles the scale and fade transition, local show and hide commands, optional dismissal rules, and synchronization with the open attribute. Native dialog behavior continues to provide the modal accessibility model.

Use Modal.show/1 and Modal.hide/1 in Phoenix JS command pipelines to open and close the modal without a server round trip.

Project details

Review the current owner and status before continuing.
Project
Component library
Owner
Alex Morgan
Status
In progress
HEEx
<div class="flex justify-center">
  <Button.button kind={:outline} phx-click={Modal.show("example-modal")}>
    Open modal
  </Button.button>
  <Modal.modal id="example-modal">
    <:title>Project details</:title>
    <:description>Review the current owner and status before continuing.</:description>
    <dl class="grid gap-4 text-sm">
      <div class="grid grid-cols-[6rem_1fr] gap-3">
        <dt class="text-muted-foreground">Project</dt>
        <dd class="font-medium">Component library</dd>
      </div>
      <div class="grid grid-cols-[6rem_1fr] gap-3">
        <dt class="text-muted-foreground">Owner</dt>
        <dd class="font-medium">Alex Morgan</dd>
      </div>
      <div class="grid grid-cols-[6rem_1fr] gap-3">
        <dt class="text-muted-foreground">Status</dt>
        <dd class="font-medium">In progress</dd>
      </div>
    </dl>
    <:footer>
      <Button.button kind={:ghost} phx-click={Modal.hide("example-modal")}>Cancel</Button.button>
      <Button.button phx-click={Modal.hide("example-modal")}>Continue</Button.button>
    </:footer>
  </Modal.modal>
</div>

Sizing

The default maximum width is max-w-lg. Pass class to replace it with a Tailwind maximum width suited to the content.

Compact modal

The content width uses max-w-sm.

Choose a maximum width that keeps forms and supporting text easy to scan.

Default modal

The content width uses the default max-w-lg.

Choose a maximum width that keeps forms and supporting text easy to scan.

Wide modal

The content width uses max-w-2xl.

Choose a maximum width that keeps forms and supporting text easy to scan.

HEEx
<div class="flex flex-wrap justify-center gap-2">
  <div :for={{label, id, width} <- [
    {"Compact", "compact", "max-w-sm"},
    {"Default", "default", nil},
    {"Wide", "wide", "max-w-2xl"}
  ]}>
    <Button.button kind={:outline} phx-click={Modal.show("#{id}-modal")}>
      {label}
    </Button.button>
    <Modal.modal id={"#{id}-modal"} class={width}>
      <:title>{label} modal</:title>
      <:description>The content width uses {width || "the default max-w-lg"}.</:description>
      <p class="text-sm leading-6 text-muted-foreground">
        Choose a maximum width that keeps forms and supporting text easy to scan.
      </p>
    </Modal.modal>
  </div>
</div>

Escape dismissal

Set close_on_escape=false when Escape must not interrupt the current workflow. The top-right close button, backdrop, and explicit actions still dismiss the modal.

Escape dismissal disabled

Pressing Escape does not dismiss this modal.

Use the top-right close button, click the backdrop, or choose the action below instead.

HEEx
<div class="flex justify-center">
  <Button.button kind={:outline} phx-click={Modal.show("escape-disabled-modal")}>
    Open Escape-protected modal
  </Button.button>
  <Modal.modal id="escape-disabled-modal" close_on_escape={false}>
    <:title>Escape dismissal disabled</:title>
    <:description>Pressing Escape does not dismiss this modal.</:description>
    <p class="text-sm leading-6 text-muted-foreground">
      Use the top-right close button, click the backdrop, or choose the action below instead.
    </p>
    <:footer>
      <Button.button phx-click={Modal.hide("escape-disabled-modal")}>Dismiss explicitly</Button.button>
    </:footer>
  </Modal.modal>
</div>

Hidden close button

Set show_close=false to omit the top-right close button when the footer or content supplies the dismissal control. Escape and backdrop dismissal remain available unless disabled separately.

Top-right close button hidden

The modal does not render its default close button.

Escape, the backdrop, and the footer action still dismiss the modal.

HEEx
<div class="flex justify-center">
  <Button.button kind={:outline} phx-click={Modal.show("hidden-close-modal")}>
    Open modal without close button
  </Button.button>
  <Modal.modal id="hidden-close-modal" show_close={false}>
    <:title>Top-right close button hidden</:title>
    <:description>The modal does not render its default close button.</:description>
    <p class="text-sm leading-6 text-muted-foreground">
      Escape, the backdrop, and the footer action still dismiss the modal.
    </p>
    <:footer>
      <Button.button phx-click={Modal.hide("hidden-close-modal")}>Done</Button.button>
    </:footer>
  </Modal.modal>
</div>

Close on click outside

Set close_on_backdrop=false when clicks outside the modal must not dismiss it. Escape, the top-right close button, and explicit actions remain available.

Outside-click dismissal disabled

Clicking outside does not dismiss this modal.

Use Escape, the top-right close button, or the action below to dismiss it.

HEEx
<div class="flex justify-center">
  <Button.button kind={:outline} phx-click={Modal.show("outside-click-disabled-modal")}>
    Open outside-click protected modal
  </Button.button>
  <Modal.modal id="outside-click-disabled-modal" close_on_backdrop={false}>
    <:title>Outside-click dismissal disabled</:title>
    <:description>Clicking outside does not dismiss this modal.</:description>
    <p class="text-sm leading-6 text-muted-foreground">
      Use Escape, the top-right close button, or the action below to dismiss it.
    </p>
    <:footer>
      <Button.button phx-click={Modal.hide("outside-click-disabled-modal")}>
        Close protected modal
      </Button.button>
    </:footer>
  </Modal.modal>
</div>

LiveView control

Bind open to an assign for server-controlled visibility. Use on_close to update that assign after the close button, Escape key, or backdrop dismisses the modal locally.

Session details

LiveView owns the open state for this modal.

Close the modal with the button, Escape key, or backdrop. The on_close command updates the server assign after every dismissal path.

HEEx
<div class="flex justify-center">
  <Button.button phx-click="open-liveview-modal">Open from LiveView</Button.button>
  <Modal.modal
    id="liveview-controlled-modal"
    open={@modal_open}
    on_close={JS.push("close-liveview-modal")}
  >
    <:title>Session details</:title>
    <:description>LiveView owns the open state for this modal.</:description>
    <p class="text-sm leading-6 text-muted-foreground">
      Close the modal with the button, Escape key, or backdrop. The on_close command updates the server assign after every dismissal path.
    </p>
    <:footer>
      <Button.button phx-click={Modal.hide("liveview-controlled-modal")}>Done</Button.button>
    </:footer>
  </Modal.modal>
</div>

Scrollable content

Long content scrolls inside the content region while the title, description, close control, and footer remain visible.

Workspace permissions

Review each permission before saving the role.
  • View projects

    This role can use view projects.

  • Create projects

    This role can use create projects.

  • Edit project settings

    This role can use edit project settings.

  • Invite members

    This role can use invite members.

  • Remove members

    This role can use remove members.

  • Manage billing

    This role can use manage billing.

  • Create API keys

    This role can use create api keys.

  • Run exports

    This role can use run exports.

  • View audit events

    This role can use view audit events.

  • Manage integrations

    This role can use manage integrations.

  • Archive projects

    This role can use archive projects.

  • Delete projects

    This role can use delete projects.

HEEx
<div class="flex justify-center">
  <Button.button kind={:outline} phx-click={Modal.show("scrolling-modal")}>
    Review permissions
  </Button.button>
  <Modal.modal id="scrolling-modal" class="max-w-xl">
    <:title>Workspace permissions</:title>
    <:description>Review each permission before saving the role.</:description>
    <ul class="-my-3 divide-y divide-border">
      <li :for={permission <- [
        "View projects",
        "Create projects",
        "Edit project settings",
        "Invite members",
        "Remove members",
        "Manage billing",
        "Create API keys",
        "Run exports",
        "View audit events",
        "Manage integrations",
        "Archive projects",
        "Delete projects"
      ]} class="py-4">
        <p class="text-sm font-medium">{permission}</p>
        <p class="mt-1 text-sm leading-6 text-muted-foreground">
          This role can use {String.downcase(permission)}.
        </p>
      </li>
    </ul>
    <:footer>
      <Button.button kind={:ghost} phx-click={Modal.hide("scrolling-modal")}>Cancel</Button.button>
      <Button.button phx-click={Modal.hide("scrolling-modal")}>Save role</Button.button>
    </:footer>
  </Modal.modal>
</div>

Add this module

terminal
mix dizzy.add modal

Component API

Modal.modal

Attributes

Attributes for Modal.modal
NameTypeDefault Values
class :any nilโ€”
close_label :string "Close modal"โ€”
close_on_backdrop :boolean trueโ€”
close_on_escape :boolean trueโ€”
id :string Requiredโ€”
on_close :any %Phoenix.LiveView.JS{ops: []}โ€”
on_open :any %Phoenix.LiveView.JS{ops: []}โ€”
open :boolean nilโ€”
rest :global nilโ€”
show_close :boolean trueโ€”

Slots

Slots for Modal.modal
NameRequired
description Optional
footer Optional
inner_block Required
title Required

JavaScript hook

DizzyModal

Opens and closes the native dialog from Phoenix JS commands or a server-rendered open value, coordinates the scale and fade transition, runs lifecycle commands, and independently configures Escape and backdrop dismissal. The browser manages focus, top-layer stacking, and background inertness.

mix dizzy.add includes the hook and performs any required asset setup automatically.

Explore Dizzy