Floating Window
v0.0.10
mix mishka.ui.gen.headless floating_window
Overview of the Phoenix and LiveView Headless Floating Window Component
The Mishka Chelekom Headless Floating Window is a Phoenix function component for a draggable panel that sits inside a positioned container. It renders a div with role="dialog" and two parts, a handle and a body. The FloatingWindow JS hook moves the panel when the handle is dragged, or when the handle has focus and an arrow key is pressed. It keeps the panel inside its container and mirrors the position into data-x and data-y.
The window is not modal and does not trap focus. A LiveView can follow it through on_move, which pushes the new coordinates when a drag ends and on every key move. Style the root with class, the title bar with handle_class and the content with body_class.
Base UI Floating Window example - draggable panel (Phoenix headless)
Base UI Floating Window example - draggable panel (Phoenix headless)
<div class="relative h-56 w-full overflow-hidden rounded-lg border border-neutral-200 bg-neutral-50 bg-[image:radial-gradient(rgba(120,120,120,0.25)_1px,transparent_0)] bg-[length:16px_16px] dark:border-neutral-800 dark:bg-neutral-900">
<.floating_window
id="baseui-floating-window"
x={24}
y={24}
label="Window"
class="absolute w-52 rounded-lg border border-neutral-200 bg-white shadow-lg dark:border-neutral-700 dark:bg-neutral-800"
handle_class="cursor-grab rounded-t-lg border-b border-neutral-200 bg-neutral-100 px-3 py-1.5 text-sm font-medium text-neutral-700 active:cursor-grabbing dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-200"
body_class="p-3 text-sm text-neutral-500 dark:text-neutral-400"
>
<:handle>Drag me</:handle>
Grab the title bar to move this panel within the box.
</.floating_window>
</div>Attributes and slots
id is required because it carries the hook. x and y are the starting offsets in pixels from the top left corner of the container, and both default to 0. label is the accessible name of the dialog. handle_label names the drag handle and defaults to "Move window".
The handle slot holds the title bar content. The default slot holds the window body and is required.
Events and controls in the handle
on_move names an event that receives %{"x" => x, "y" => y} as whole pixels when a drag ends and after each arrow key. If you track the window on the server, store the coordinates from this event and pass them back as x and y, so a re-render keeps the window where it was.
Buttons, links, inputs, textareas and selects inside the handle do not start a drag, so a close button in the title bar keeps working. Add data-no-drag to any other element that should not start one.
<.floating_window
id="notes-window"
x={@pos.x}
y={@pos.y}
on_move="moved"
label="Notes"
class="absolute w-56 rounded-lg border bg-white shadow-lg"
handle_class="flex cursor-grab items-center justify-between px-3 py-1.5"
>
<:handle>
<span>Notes</span>
<button type="button" phx-click={JS.hide(to: "#notes-window")} aria-label="Close">
Close
</button>
</:handle>
Window content.
</.floating_window>def handle_event("moved", %{"x" => x, "y" => y}, socket) do
{:noreply, assign(socket, :pos, %{x: x, y: y})}
endParts & data attributes
The root is marked data-part="window" and carries data-x, data-y and, during a drag, data-dragging. The other parts are handle and body, with the structural classes chelekom-floating-window__handle and chelekom-floating-window__body. The hook writes the position to the inline left and top of the root.
Accessibility
The handle is a focusable element with role="button", aria-label and aria-roledescription="draggable window handle". With the handle focused, the arrow keys move the window by 10 pixels, or by 1 pixel with Shift held. This gives keyboard users an alternative to dragging, as WCAG 2.5.7 asks. There is no formal ARIA pattern for a movable non-modal window, so the root uses the dialog role without a focus trap.
Attributes
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
id
|
string
| — | — | Yes | Unique id (carries the FloatingWindow hook) |
x
|
integer
|
0
| — | No | Initial left offset in px (within the stage) |
y
|
integer
|
0
| — | No | Initial top offset in px (within the stage) |
on_move
|
string
| — | — | No | Event pushed with %{x: _, y: _} on release |
label
|
string
| — | — | No | Accessible label (role=dialog) |
handle_label
|
string
|
Move window
| — | No | Accessible label for the drag handle |
class
|
any
| — | — | No | Extra classes for the root |
handle_class
|
any
| — | — | No | Extra classes for the handle |
body_class
|
any
| — | — | No | Extra classes for the body |
rest
|
global
| — | — | No | — |
Slots
| Name | Required | Description |
|---|---|---|
handle
| No | Drag-handle content (e.g. a title bar) |
inner_block
| Yes | Window body |
Options
Each option takes one or more of its values, separated by commas.
| Option | Values |
|---|---|
--type
|
floating_window
|
--module
| A module name of your own |
What it generates
mix mishka.ui.gen.headless floating_window