Drawer

v0.0.10

v0.0.10StableHeadlessStatelessAccessibleJSApache-2.0
Installation mix mishka.ui.gen.headless drawer
Source View Source

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)

heex
<.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.

Partdata-partState / ARIAPurpose
Triggertriggeraria-haspopup, aria-expanded, aria-controls, data-popup-openThe button that opens the drawer.
Swipe areaswipe-areaaria-hidden, data-open, data-closed, data-swiping, data-swipe-directionOpt-in edge strip; drag from it to open a closed drawer.
Backdropbackdroparia-hidden, data-swiping, --drawer-swipe-progressThe overlay behind the popup; fades with swipe progress.
Popuppopuprole="dialog", aria-modal, data-side, data-open, data-closed, data-swiping, data-expanded, data-starting-style, data-ending-styleThe drawer panel itself; focus-trapped when modal.
Handlehandlearia-hiddenOptional drag-handle pill for bottom-sheet UX.
Title / Descriptiontitle / descriptionaria-labelledby / aria-describedbyAnnounced 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.