Sheet

Slide-in panel anchored to any edge of the viewport, for filters, navigation, and detail views.

Basic

.heex
<.button phx-click={Fluxon.open_dialog("basic-sheet")}>Open sheet</.button>

<.sheet id="basic-sheet" placement="right" class="w-96">
  <h3 class="text-lg font-semibold">Sheet title</h3>
  <p class="text-sm text-foreground-soft mt-2">
    Use this space for supplementary content that benefits from edge anchoring.
  </p>

  <div class="flex justify-end gap-3 mt-6">
    <.button phx-click={Fluxon.close_dialog("basic-sheet")}>Cancel</.button>
    <.button variant="solid" color="primary">Save changes</.button>
  </div>
</.sheet>

Placement

placement sets the anchor edge and matching slide animation. Left and right sheets fill the viewport height; top and bottom fill the width.

.heex
<.button phx-click={Fluxon.open_dialog("sheet-left")}>Left</.button>
<.button phx-click={Fluxon.open_dialog("sheet-right")}>Right</.button>
<.button phx-click={Fluxon.open_dialog("sheet-top")}>Top</.button>
<.button phx-click={Fluxon.open_dialog("sheet-bottom")}>Bottom</.button>

<.sheet id="sheet-left" placement="left" class="w-80">Left drawer</.sheet>
<.sheet id="sheet-right" placement="right" class="w-80">Right panel</.sheet>
<.sheet id="sheet-top" placement="top" class="h-48">Top sheet</.sheet>
<.sheet id="sheet-bottom" placement="bottom" class="h-64">Bottom sheet</.sheet>

Sizing

Use class to set the perpendicular dimension. Tailwind sizing utilities like w-80, w-[28rem], or max-w-xl all work.

.heex
<.button phx-click={Fluxon.open_dialog("sheet-w-80")}>Compact</.button>
<.button phx-click={Fluxon.open_dialog("sheet-w-28rem")}>Comfortable</.button>
<.button phx-click={Fluxon.open_dialog("sheet-max-xl")}>Wide</.button>

<.sheet id="sheet-w-80" placement="right" class="w-80">w-80</.sheet>
<.sheet id="sheet-w-28rem" placement="right" class="w-[28rem]">w-[28rem]</.sheet>
<.sheet id="sheet-max-xl" placement="right" class="w-full max-w-xl">max-w-xl</.sheet>

Custom backdrop

backdrop_class overrides the default scrim with tinted overlays, frosted glass, or a lighter dim when stacking on already-darkened content.

.heex
<.button phx-click={Fluxon.open_dialog("backdrop-tinted")}>Tinted</.button>
<.button phx-click={Fluxon.open_dialog("backdrop-frosted")}>Frosted glass</.button>
<.button phx-click={Fluxon.open_dialog("backdrop-subtle")}>Subtle dim</.button>

<.sheet id="backdrop-tinted" placement="right" class="w-96" backdrop_class="bg-blue-500/30">
  Tinted overlay
</.sheet>

<.sheet
  id="backdrop-frosted"
  placement="right"
  class="w-96"
  backdrop_class="bg-black/10 backdrop-blur-md"
>
  Frosted glass
</.sheet>

<.sheet id="backdrop-subtle" placement="right" class="w-96" backdrop_class="bg-black/10">
  Subtle dim
</.sheet>

Custom close control

Set hide_close_button to drop the built-in close glyph and provide your own dismissal in the body or footer.

.heex
<.button phx-click={Fluxon.open_dialog("custom-close-sheet")}>Open sheet</.button>

<.sheet id="custom-close-sheet" placement="right" class="w-96" hide_close_button>
  <h3 class="text-lg font-semibold">Confirm changes</h3>
  <p class="text-sm text-foreground-soft mt-2">
    Review the summary below, then choose Confirm or Cancel.
  </p>

  <div class="flex justify-end gap-3 mt-6">
    <.button phx-click={Fluxon.close_dialog("custom-close-sheet")}>Cancel</.button>
    <.button variant="solid" color="primary">Confirm</.button>
  </div>
</.sheet>

Disable dismissal shortcuts

Turn off close_on_esc and close_on_outside_click when the sheet hosts nested controls like code editors, video players, or drag interactions where Escape or backdrop clicks would conflict.

.heex
<.button phx-click={Fluxon.open_dialog("no-dismissal-sheet")}>Open sheet</.button>

<.sheet
  id="no-dismissal-sheet"
  placement="right"
  class="w-96"
  close_on_esc={false}
  close_on_outside_click={false}
>
  <h3 class="text-lg font-semibold">Focused workspace</h3>
  <p class="text-sm text-foreground-soft mt-2">
    Escape and backdrop clicks are disabled. Use the close button to dismiss.
  </p>
</.sheet>

Prevent closing

prevent_closing disables every client-side dismissal: Escape, backdrop click, and the built-in close button. The sheet only closes from the server, or by flipping a bound open assign.

.heex
<.button phx-click={Fluxon.open_dialog("locked-sheet")}>Start onboarding</.button>

<.sheet id="locked-sheet" placement="right" class="w-96" prevent_closing>
  <h3 class="text-lg font-semibold">Finish setup</h3>
  <p class="text-sm text-foreground-soft mt-2">
    Complete this step before continuing. The only way out is the button below.
  </p>

  <div class="flex justify-end mt-6">
    <.button
      variant="solid"
      color="primary"
      phx-click={Fluxon.close_dialog("locked-sheet")}
    >
      Mark complete
    </.button>
  </div>
</.sheet>

Loading state

Render the loading visual as the sheet's default content so it shows the moment the dialog opens; the server swaps in real content when ready.

.heex
<%!-- The sheet renders the skeleton as its default content, so it is --%>
<%!-- on screen the moment Fluxon.open_dialog runs. on_close resets the --%>
<%!-- state so reopening always starts back in the loading view. --%>
<.button phx-click={
  JS.push("load_user", value: %{id: 1}) |> Fluxon.open_dialog("user-sheet")
}>
  View customer
</.button>

<.sheet
  id="user-sheet"
  placement="right"
  class="w-[28rem]"
  on_close={JS.push("reset_user")}
>
  <div :if={!@user} class="space-y-3">
    <div class="size-14 rounded-full bg-zinc-200 animate-pulse" />
    <div class="h-4 w-2/3 rounded bg-zinc-200 animate-pulse" />
  </div>

  <div :if={@user}>
    <h3>{@user.name}</h3>
    <p>{@user.email}</p>
  </div>
</.sheet>
.ex
def handle_event("load_user", %{"id" => _id}, socket) do
  # Kick off the real fetch here; send {:user_loaded, user} back when ready
  {:noreply, assign(socket, user: nil)}
end

def handle_info({:user_loaded, user}, socket) do
  {:noreply, assign(socket, user: user)}
end

def handle_event("reset_user", _, socket) do
  {:noreply, assign(socket, user: nil)}
end

Dynamic content per trigger

Multiple buttons open the same sheet, each pushing a different load event so the server decides what to render. Pair open_dialog with JS.push in the click handler, and use on_close to reset the context.

.heex
<%!-- All buttons open the same sheet, but each pushes a different load --%>
<%!-- event so the server decides what to render. on_close resets context --%>
<%!-- so the next opening starts back in the loading state. --%>
<.button phx-click={
  JS.push("load_filters", value: %{type: "users"})
  |> Fluxon.open_dialog("filter-sheet")
}>
  User filters
</.button>

<.button phx-click={
  JS.push("load_filters", value: %{type: "dates"})
  |> Fluxon.open_dialog("filter-sheet")
}>
  Date filters
</.button>

<.sheet
  id="filter-sheet"
  placement="right"
  class="w-96"
  on_close={JS.push("clear_filters")}
>
  <div :if={!@context} class="flex items-center gap-3 text-sm text-foreground-softer">
    <.loading variant="ring-bg" class="size-5" /> Loading filters...
  </div>

  <div :if={@context}>
    <h3>{@context.title}</h3>
    <ul>
      <li :for={item <- @context.items}>{item}</li>
    </ul>
  </div>
</.sheet>
.ex
def handle_event("load_filters", %{"type" => _type}, socket) do
  # Kick off the real fetch here; send {:filters_loaded, context} back when ready
  {:noreply, assign(socket, context: nil)}
end

def handle_info({:filters_loaded, context}, socket) do
  {:noreply, assign(socket, context: context)}
end

def handle_event("clear_filters", _, socket) do
  {:noreply, assign(socket, context: nil)}
end

Form submission with validation

Form bound to a LiveView changeset. phx-change validates on every keystroke; phx-submit closes the sheet on success and shows field-level errors otherwise.

.heex
<.button phx-click={Fluxon.open_dialog("invite-sheet")}>Invite teammate</.button>

<.sheet id="invite-sheet" placement="right" class="w-[28rem]">
  <.form for={@form} phx-change="validate" phx-submit="save">
    <.input field={@form[:name]} label="Full name" />
    <.input field={@form[:email]} type="email" label="Email" />
    <.select field={@form[:role]} label="Role" native options={@roles} />
    <.input field={@form[:message]} type="textarea" label="Welcome message" />

    <.button type="button" phx-click={Fluxon.close_dialog("invite-sheet")}>
      Cancel
    </.button>
    <.button type="submit" variant="solid">Send invite</.button>
  </.form>
</.sheet>
.ex
def handle_event("validate", %{"user" => _params}, socket) do
  # Re-run validation and reassign the form
  {:noreply, socket}
end

def handle_event("save", %{"user" => _params}, socket) do
  # Send the invite, then dismiss the sheet
  {:noreply, Fluxon.close_dialog(socket, "invite-sheet")}
end

Two-column workflow

Wide sheet with a scrollable form on one side and a sticky summary sidebar on the other. Fits checkout, export, and shipping flows.

.heex
<.button phx-click={Fluxon.open_dialog("shipping-sheet")}>
  Create shipping label
</.button>

<%!-- Mobile-first: full width with a single scroll on small screens; on lg --%>
<%!-- the sheet caps at 58rem and splits into two independently-scrolling --%>
<%!-- columns (form on the left, summary on the right). --%>
<.sheet
  id="shipping-sheet"
  placement="right"
  class="w-full lg:w-[58rem] lg:max-w-[95vw] p-0 flex flex-col"
  hide_close_button
  prevent_closing={@status == :submitting}
>
  <.form for={@form} phx-change="validate" phx-submit="save" class="flex-1 flex flex-col">
    <header class="shrink-0 flex items-center justify-between gap-3 px-4 sm:px-6 py-4 border-b border-base">
      <div>
        <h2 class="text-lg font-semibold">Create shipping label</h2>
        <p class="text-xs text-foreground-soft">Order #12567 · 2 items</p>
      </div>
      <.button
        type="button"
        variant="ghost"
        size="icon-sm"
        phx-click={Fluxon.close_dialog("shipping-sheet")}
        aria-label="Close"
      >
        <.icon name="huge-cancel-01" class="icon" />
      </.button>
    </header>

    <%!-- Outer body switches scroll containers at the lg breakpoint. --%>
    <div class="flex-1 grid grid-cols-1 lg:grid-cols-[1fr_22rem] overflow-y-auto lg:overflow-hidden">
      <div class="px-4 sm:px-6 py-5 space-y-6 lg:overflow-y-auto lg:border-r lg:border-base">
        <section class="space-y-4">
          <h3 class="text-sm font-semibold">Shipping Details</h3>

          <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
            <.select field={@form[:carrier]} label="Carrier service" options={@carriers}>
              <:inner_prefix>
                <.icon name="huge-truck-delivery" class="size-4" />
              </:inner_prefix>
            </.select>
            <.select field={@form[:service]} label="Service type" options={@services} />
          </div>

          <.select
            field={@form[:insurance]}
            label="Shipping insurance"
            sublabel="Optional"
            options={@insurance}
          />
        </section>

        <section class="space-y-4">
          <h3 class="text-sm font-semibold">Item Package</h3>

          <div class="rounded-lg border border-base p-4 space-y-5">
            <%!-- Avatar + info stay inline; weight drops below them on phones. --%>
            <div class="flex flex-col sm:flex-row sm:items-center gap-4">
              <div class="flex items-center gap-3 flex-1 min-w-0">
                <img src={@item.image} class="size-12 rounded-md" />
                <div class="flex-1 min-w-0">
                  <div class="text-sm font-medium truncate">{@item.name}</div>
                  <div class="text-xs text-foreground-soft">{@item.variant}</div>
                </div>
              </div>
              <div class="w-full sm:w-36 sm:shrink-0">
                <.input field={@form[:item_weight]} label="Item weight" size="sm">
                  <:outer_suffix class="bg-emphasis px-2 text-xs">kg</:outer_suffix>
                </.input>
              </div>
            </div>

            <.tabs id="pkg-tabs">
              <.tabs_list variant="default" active_tab="custom-pkg">
                <:tab name="custom-pkg">Custom Package</:tab>
                <:tab name="carrier-pkg">Carrier Package</:tab>
              </.tabs_list>

              <.tabs_panel name="custom-pkg" active class="pt-4 space-y-4">
                <.input
                  field={@form[:pkg_name]}
                  label="Package name"
                  placeholder="Type package name"
                />

                <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
                  <.select
                    field={@form[:pkg_type]}
                    label="Package type"
                    options={@package_types}
                  />
                  <.input
                    field={@form[:pkg_weight]}
                    type="number"
                    label="Total weight"
                    sublabel="Include package"
                  >
                    <:outer_suffix class="bg-emphasis px-3">kg</:outer_suffix>
                  </.input>
                </div>

                <%!-- L, W, H share the same shape: a divider-styled inner_prefix --%>
                <%!-- for the axis letter and a static inner_suffix for the unit. --%>
                <div>
                  <label class="block text-sm font-medium mb-1.5">Dimensions</label>
                  <div class="grid grid-cols-3 gap-2 sm:gap-4">
                    <.input
                      :for={axis <- [:pkg_l, :pkg_w, :pkg_h]}
                      field={@form[axis]}
                      type="number"
                      placeholder="00"
                    >
                      <:inner_prefix class="border-r border-base pr-2 font-medium">
                        {axis |> to_string() |> String.last() |> String.upcase()}
                      </:inner_prefix>
                      <:inner_suffix>cm</:inner_suffix>
                    </.input>
                  </div>
                </div>
              </.tabs_panel>

              <.tabs_panel name="carrier-pkg" class="pt-4">
                Use a carrier-supplied box. Dimensions and weight come from the
                carrier catalog at print time.
              </.tabs_panel>
            </.tabs>
          </div>
        </section>
      </div>

      <aside class="px-4 sm:px-6 py-5 space-y-6 border-t border-base lg:border-t-0 lg:overflow-y-auto lg:bg-emphasis">
        <section>
          <h3 class="text-sm font-semibold">Summary</h3>
          <dl class="mt-3 space-y-2 text-sm">
            <div class="flex justify-between">
              <dt class="text-foreground-soft">Subtotal</dt>
              <dd class="tabular-nums">${@summary.subtotal}</dd>
            </div>
            <div class="flex justify-between">
              <dt class="text-foreground-soft">Discount</dt>
              <dd class="tabular-nums">${@summary.discount}</dd>
            </div>
            <div class="flex justify-between pt-2 border-t border-base">
              <dt class="font-medium">Total</dt>
              <dd class="font-semibold tabular-nums">${@summary.total}</dd>
            </div>
          </dl>
        </section>

        <.date_picker field={@form[:shipping_date]} label="Shipping date" />

        <section>
          <div class="flex items-center justify-between">
            <h3 class="text-sm font-semibold">Return address</h3>
            <.button type="button" variant="ghost" size="icon-sm" aria-label="Edit">
              <.icon name="huge-pencil" class="icon" />
            </.button>
          </div>
          <div class="mt-2 text-sm text-foreground-soft">
            <div class="font-medium text-foreground">{@return_address.name}</div>
            {@return_address.line1},<br /> {@return_address.line2}
          </div>
        </section>
      </aside>
    </div>

    <footer class="shrink-0 flex items-center justify-end gap-3 px-4 sm:px-6 py-3 border-t border-base">
      <.button
        type="button"
        phx-click={Fluxon.close_dialog("shipping-sheet")}
        disabled={@status == :submitting}
      >
        Cancel
      </.button>
      <.button
        type="submit"
        variant="solid"
        color="primary"
        disabled={@status == :submitting}
      >
        <.loading :if={@status == :submitting} class="size-4" />
        <.icon :if={@status == :idle} name="huge-tag-01" class="size-4" />
        {if @status == :submitting, do: "Buying label...", else: "Buy shipping label"}
      </.button>
    </footer>
  </.form>
</.sheet>
.ex
def handle_event("validate", %{"shipment" => _params}, socket) do
  # Re-run validation and reassign the form
  {:noreply, socket}
end

def handle_event("save", %{"shipment" => _params}, socket) do
  # Kick off the label purchase; flip to :submitting so the button shows a spinner
  {:noreply, assign(socket, status: :submitting)}
end

def handle_info(:label_purchased, socket) do
  {:noreply,
   socket
   |> assign(status: :idle)
   |> Fluxon.close_dialog("shipping-sheet")}
end

Server-driven close

Lock down dismissal with prevent_closing and let the server close the sheet once the workflow finishes.

.heex
<.sheet id="export-sheet" placement="right" class="w-[28rem]" prevent_closing>
  <h3>Export workspace</h3>

  <.form for={%{}} as={:export} phx-submit="start_export">
    <%!-- Radios are plain HTML  -  selection styling is pure CSS via --%>
    <%!-- Tailwind's has-[:checked] variant, so no roundtrip is needed. --%>
    <label
      :for={{label, value} <- @formats}
      class="border has-[:checked]:border-violet-500"
    >
      <input type="radio" name="export[format]" value={value} />
      {label}
    </label>

    <.button type="submit" disabled={@processing?}>Start export</.button>
  </.form>
</.sheet>
.ex
def handle_event("start_export", %{"export" => %{"format" => _format}}, socket) do
  # Kick off the real export here; send {:export_result, result} back when ready
  {:noreply, assign(socket, processing?: true)}
end

def handle_info({:export_result, {:ok, _file}}, socket) do
  {:noreply,
   socket
   |> assign(processing?: false)
   |> Fluxon.close_dialog("export-sheet")}
end

Sticky header and footer

Lay the sheet out as a flex column with flex flex-col, then mark the header and footer as shrink-0 and let the middle region scroll with flex-1 overflow-y-auto.

.heex
<.button phx-click={Fluxon.open_dialog("sticky-sheet")}>Open settings</.button>

<.sheet id="sticky-sheet" placement="right" class="w-96 flex flex-col" hide_close_button>
  <header class="border-b border-base -mx-6 px-6 pb-4 mb-4 shrink-0">
    <h2 class="text-lg font-semibold">Workspace settings</h2>
  </header>

  <div class="flex-1 overflow-y-auto -mx-6 px-6 space-y-3">
    <div :for={i <- 1..15} class="py-3 border-b border-base last:border-0">
      <div class="font-medium">Setting {i}</div>
      <div class="text-sm text-foreground-soft">A short description for this option.</div>
    </div>
  </div>

  <footer class="border-t border-base -mx-6 px-6 pt-4 mt-4 shrink-0 flex justify-end gap-3">
    <.button phx-click={Fluxon.close_dialog("sticky-sheet")}>Cancel</.button>
    <.button variant="solid" color="primary">Save</.button>
  </footer>
</.sheet>

A left-anchored sheet is the canonical app menu. Pair it with a hamburger trigger so navigation collapses neatly on small screens.

.heex
<.button phx-click={Fluxon.open_dialog("nav-drawer")}>
  <.icon name="hero-bars-3" class="size-5" /> Menu
</.button>

<.sheet id="nav-drawer" placement="left" class="w-72">
  <div class="font-semibold text-base mb-6">Acme</div>

  <nav class="space-y-1 text-sm">
    <a href="#" class="flex items-center gap-3 px-2 py-2 rounded-md hover:highlight">
      <.icon name="hero-home" class="size-5" /> Home
    </a>
    <a href="#" class="flex items-center gap-3 px-2 py-2 rounded-md hover:highlight">
      <.icon name="hero-inbox" class="size-5" /> Inbox
    </a>
    <a href="#" class="flex items-center gap-3 px-2 py-2 rounded-md hover:highlight">
      <.icon name="hero-user-group" class="size-5" /> Customers
    </a>
    <a href="#" class="flex items-center gap-3 px-2 py-2 rounded-md hover:highlight">
      <.icon name="hero-cog-6-tooth" class="size-5" /> Settings
    </a>
  </nav>
</.sheet>

Notification panel

Top-anchored sheets are full width by default. Wrap the body in max-w-* mx-auto to keep content readable on wide displays.

.heex
<.button phx-click={Fluxon.open_dialog("notifications-sheet")}>
  <.icon name="hero-bell" class="size-4" /> Notifications
</.button>

<.sheet id="notifications-sheet" placement="top" class="h-auto max-h-[80vh]">
  <div class="max-w-2xl mx-auto">
    <header class="flex items-center justify-between mb-4">
      <h3 class="text-lg font-semibold">Notifications</h3>
      <.button variant="ghost" size="sm">Mark all as read</.button>
    </header>

    <ul class="divide-y divide-base">
      <li class="flex gap-3 py-3">
        <.icon name="hero-check-circle-solid" class="size-5 text-success shrink-0 mt-0.5" />
        <div>
          <div class="text-sm font-medium">Deployment succeeded</div>
          <div class="text-sm text-foreground-softer">main shipped to production 4 minutes ago.</div>
        </div>
      </li>
      <li class="flex gap-3 py-3">
        <.icon name="hero-information-circle-solid" class="size-5 text-info shrink-0 mt-0.5" />
        <div>
          <div class="text-sm font-medium">Weekly digest is ready</div>
          <div class="text-sm text-foreground-softer">12 new comments and 3 mentions this week.</div>
        </div>
      </li>
      <li class="flex gap-3 py-3">
        <.icon name="hero-exclamation-triangle-solid" class="size-5 text-warning shrink-0 mt-0.5" />
        <div>
          <div class="text-sm font-medium">Trial ends in 3 days</div>
          <div class="text-sm text-foreground-softer">Add a payment method to keep your workspace.</div>
        </div>
      </li>
    </ul>
  </div>
</.sheet>

Mobile action sheet

A bottom sheet with rounded-t-xl mirrors the iOS and Android action-sheet pattern. Group the actions semantically: primary actions first, destructive actions on their own surface, and a Cancel that stands clearly apart.

.heex
<.button phx-click={Fluxon.open_dialog("row-actions")}>Row actions</.button>

<.sheet id="row-actions" placement="bottom" class="h-auto rounded-t-xl">
  <div class="max-w-md mx-auto space-y-3">
    <div class="rounded-lg bg-surface shadow-sm divide-y divide-base">
      <button class="w-full flex items-center gap-3 px-4 py-3.5 text-left text-sm">
        <.icon name="hero-share" class="size-5" /> Share
      </button>
      <button class="w-full flex items-center gap-3 px-4 py-3.5 text-left text-sm">
        <.icon name="hero-document-duplicate" class="size-5" /> Duplicate
      </button>
      <button class="w-full flex items-center gap-3 px-4 py-3.5 text-left text-sm">
        <.icon name="hero-archive-box" class="size-5" /> Archive
      </button>
    </div>

    <div class="rounded-lg bg-surface shadow-sm">
      <button class="w-full flex items-center gap-3 px-4 py-3.5 text-left text-sm text-danger">
        <.icon name="hero-trash" class="size-5" /> Delete invoice
      </button>
    </div>

    <.button class="w-full" phx-click={Fluxon.close_dialog("row-actions")}>Cancel</.button>
  </div>
</.sheet>

Stacked with a modal

Sheets and modals share the same stacking system, so opening a modal on top of a sheet works without extra wiring. Focus, keyboard, and outside-click handling always belong to the topmost dialog.

.heex
<.button phx-click={Fluxon.open_dialog("edit-sheet")}>Edit user</.button>

<.sheet id="edit-sheet" placement="right" class="w-96">
  <h3 class="text-lg font-semibold">Edit user</h3>
  <p class="text-sm text-foreground-soft mt-1">You have unsaved changes.</p>

  <.input name="name" label="Name" value="Jane Doe" />
  <.input name="email" type="email" label="Email" value="jane@acme.com" />

  <div class="flex justify-between gap-3 mt-6">
    <.button color="danger" phx-click={Fluxon.open_dialog("confirm-discard")}>
      Discard
    </.button>
    <.button variant="solid" color="primary">Save</.button>
  </div>
</.sheet>

<.modal id="confirm-discard" class="w-full max-w-sm p-6">
  <h3 class="text-lg font-semibold">Discard changes?</h3>
  <p class="text-sm text-foreground-soft mt-2">
    Your edits will be lost. This cannot be undone.
  </p>

  <div class="flex justify-end gap-3 mt-6">
    <.button phx-click={Fluxon.close_dialog("confirm-discard")}>Keep editing</.button>
    <.button
      variant="solid"
      color="danger"
      phx-click={Fluxon.close_dialog("confirm-discard") |> Fluxon.close_dialog("edit-sheet")}
    >
      Discard
    </.button>
  </div>
</.modal>