Drawer
v0.0.10
mix mishka.ui.gen.headless drawer
Overview of the Phoenix and LiveView Headless Drawer Component
The Mishka Chelekom Headless Drawer ships zero opinionated styling - only the semantic markup, full WAI-ARIA wiring, a focus trap, swipe-to-dismiss, and paired-presence state (data-open/data-closed). Every part - trigger, swipe-area, backdrop, viewport, popup, handle, title, description, content and footer - is addressable through a *_class attribute, so you style it entirely with your own Tailwind classes while the accessibility and behavior keep working underneath.
Base UI Drawer example (Phoenix headless)
Base UI Drawer example (Phoenix headless)
<.drawer
id="baseui-drawer-hero"
side="right"
swipe_direction="right"
trigger_class="flex h-8 items-center justify-center gap-2 border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 px-3 text-sm font-normal text-neutral-950 dark:text-white select-none hover:bg-neutral-100 dark:hover:bg-neutral-800"
backdrop_class="[--backdrop-opacity:0.2] [--bleed:3rem] dark:[--backdrop-opacity:0.7] fixed inset-0 min-h-dvh bg-black opacity-[calc(var(--backdrop-opacity)*(1-var(--drawer-swipe-progress)))] transition-opacity duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-[swiping]:duration-0 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0"
viewport_class="[--viewport-padding:0px] fixed inset-0 flex items-stretch justify-end p-(--viewport-padding)"
popup_class="[--bleed:3rem] h-full w-[calc(20rem+3rem)] max-w-[calc(100vw-3rem+3rem)] -mr-[3rem] border-l border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 p-6 pr-[calc(1.5rem+3rem)] text-neutral-950 dark:text-white outline-none shadow-[0.25rem_0.25rem_0] shadow-black/12 overflow-y-auto overscroll-contain touch-auto [transform:translateX(var(--drawer-swipe-movement-x))] transition-transform duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-[ending-style]:[transform:translateX(calc(100%-var(--bleed)+var(--viewport-padding)+2px))] data-[starting-style]:[transform:translateX(calc(100%-var(--bleed)+var(--viewport-padding)+2px))]"
content_class="mx-auto w-full max-w-[32rem]"
title_class="mb-1 text-base font-bold"
description_class="mb-6 text-sm text-neutral-600 dark:text-neutral-400"
footer_class="flex justify-end gap-3"
>
<:trigger>Open drawer</:trigger>
<:title>Drawer</:title>
<:description>
This is a drawer that slides in from the side. You can swipe to dismiss it.
</:description>
<p class="text-sm text-neutral-600 dark:text-neutral-400">Nothing new since your last visit.</p>
<:close>
<button
type="button"
data-close
class="flex h-8 items-center gap-2 border border-neutral-950 dark:border-white bg-white dark:bg-neutral-950 px-3 text-sm text-neutral-950 dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800"
>
Close
</button>
</:close>
</.drawer>Parts & data attributes
Each part carries a stable data-part and a structural chelekom-drawer__* class you can target, plus paired-presence state for open/closed and swipe transitions.
| Part | data-part | State / ARIA | Purpose |
|---|---|---|---|
| Trigger | trigger | aria-haspopup, aria-expanded, aria-controls, data-popup-open | The button that opens the drawer. |
| Swipe area | swipe-area | aria-hidden, data-open, data-closed, data-swiping, data-swipe-direction | Opt-in edge strip; drag from it to open a closed drawer. |
| Backdrop | backdrop | aria-hidden, data-swiping, --drawer-swipe-progress | The overlay behind the popup; fades with swipe progress. |
| Popup | popup | role="dialog", aria-modal, data-side, data-open, data-closed, data-swiping, data-expanded, data-starting-style, data-ending-style | The drawer panel itself; focus-trapped when modal. |
| Handle | handle | aria-hidden | Optional drag-handle pill for bottom-sheet UX. |
| Title / Description | title / description | aria-labelledby / aria-describedby | Announced to screen readers; wired automatically. |
Accessibility
Follows the WAI-ARIA Dialog (Modal) pattern: focus is trapped inside the open drawer and restored to the trigger on close, Escape closes it, and an outside click dismisses it (both configurable). Swipe-to-dismiss is pointer-only and has no keyboard equivalent, so a visible close control is always required. The title and description slots are announced to screen readers.
Attributes
| Name | Type | Default | Values | Required | Description |
|---|---|---|---|---|---|
id
|
string
| — | — | Yes | Unique id (anchors aria + indent scope + hook) |
open
|
boolean
|
false
| — | No | Initial/controlled open state |
side
|
string
|
right
|
leftrighttopbottom
| No | Edge the drawer slides from |
swipe_direction
|
string
| — |
updownleftright
| No | Dismiss direction (defaults from side) |
modal
|
any
|
true
| — | No | true | false | "trap-focus" — focus trap / scroll lock / backdrop |
dismissible
|
boolean
|
true
| — | No | Whether an outside (backdrop) click dismisses |
close_on_escape
|
boolean
|
true
| — | No | Whether Escape closes |
swipe
|
boolean
|
true
| — | No | Enable swipe-to-dismiss |
swipe_area
|
boolean
|
false
| — | No | Render an edge swipe-area that opens a closed drawer |
swipe_area_direction
|
string
| — | — | No | Explicit open direction for the swipe-area |
snap_points
|
list
|
[]
| — | No | Bottom-sheet snap points (fraction ≤1, px >1, "Npx", "Nrem"); vertical only |
default_snap_point
|
any
| — | — | No | Initial active snap point (defaults to the first) |
snap_to_sequential_points
|
boolean
|
false
| — | No | One snap point per flick (no velocity skipping) |
on_open_change
|
string
| — | — | No | LiveView event pushed on open/close ({open}) |
on_open_change_target
|
string
| — | — | No | Optional pushEventTo target for on_open_change |
on_snap_point_change
|
string
| — | — | No | LiveView event pushed when the active snap point changes ({value}) |
on_snap_point_change_target
|
string
| — | — | No | Optional pushEventTo target for on_snap_point_change |
labelledby
|
string
| — | — | No | Override aria-labelledby |
describedby
|
string
| — | — | No | Override aria-describedby |
class
|
any
| — | — | No | Extra classes for the root |
trigger_class
|
any
| — | — | No | Extra classes for the trigger part |
swipe_area_class
|
any
| — | — | No | Extra classes for the swipe-area part |
backdrop_class
|
any
| — | — | No | Extra classes for the backdrop part |
viewport_class
|
any
| — | — | No | Extra classes for the viewport part |
popup_class
|
any
| — | — | No | Extra classes for the popup part |
handle_class
|
any
| — | — | No | Extra classes for the handle part |
title_class
|
any
| — | — | No | Extra classes for the title part |
description_class
|
any
| — | — | No | Extra classes for the description part |
content_class
|
any
| — | — | No | Extra classes for the content part |
footer_class
|
any
| — | — | No | Extra classes for the footer part |
rest
|
global
| — | — | No | — |
Slots
| Name | Required | Description |
|---|---|---|
trigger
| No | The element that opens the drawer |
swipe_area_inner
| No | Optional content rendered inside the edge swipe-area (e.g. a label) |
title
| No | Accessible title (wired to aria-labelledby) |
description
| No | Accessible description (wired to aria-describedby) |
handle
| No | Optional drag-handle pill for bottom-sheet UX |
inner_block
| Yes | Drawer body |
close
| No | Footer with confirm/cancel buttons (use data-close to close) |
Options
Each option takes one or more of its values, separated by commas.
| Option | Values |
|---|---|
--type
|
drawer
|
--module
| A module name of your own |
What it generates
mix mishka.ui.gen.headless drawer