Skip to content

cv-drawer

Slide-out panel dialog anchored to a viewport edge, used for navigation, forms, or supplementary content.

Headless: createDrawer

Usage

View source
html
<div class="drawer-demo-shell" data-demo="drawer" data-live-demo-height="440">
  <section class="drawer-demo-stage" aria-labelledby="drawer-demo-title">
    <aside class="drawer-demo-rail" aria-label="Vault sections">
      <span class="drawer-demo-mark">CV</span>
      <a class="drawer-demo-rail-item drawer-demo-rail-item--active" href="#drawer-vault">Vault</a>
      <a class="drawer-demo-rail-item" href="#drawer-devices">Devices</a>
      <a class="drawer-demo-rail-item" href="#drawer-audit">Audit</a>
    </aside>

    <main class="drawer-demo-workspace">
      <header class="drawer-demo-toolbar">
        <div class="drawer-demo-title-block">
          <span class="drawer-demo-kicker">Drawer surface</span>
          <h3 id="drawer-demo-title">Route-level panels without losing context</h3>
        </div>

        <div class="drawer-demo-actions" aria-label="Drawer examples">
          <cv-drawer class="drawer-demo-drawer drawer-demo-drawer--nav" placement="start" drag-to-close>
            <span slot="trigger">Navigation</span>
            <span slot="title">Vault routes</span>
            <span slot="description">A start drawer works as a temporary navigation layer.</span>
            <nav class="drawer-demo-nav" aria-label="Drawer navigation">
              <a class="drawer-demo-nav-link drawer-demo-nav-link--active" href="#drawer-vault">
                <strong>Records</strong>
                <span>Credentials, recovery seeds, and secure notes</span>
              </a>
              <a class="drawer-demo-nav-link" href="#drawer-devices">
                <strong>Devices</strong>
                <span>Pairing state and hardware trust boundary</span>
              </a>
              <a class="drawer-demo-nav-link" href="#drawer-audit">
                <strong>Audit trail</strong>
                <span>Recent access and policy changes</span>
              </a>
            </nav>
          </cv-drawer>

          <cv-drawer
            id="drawer-demo-policy"
            class="drawer-demo-drawer drawer-demo-drawer--settings"
            placement="end"
            initial-focus-id="drawer-demo-save"
          >
            <span slot="trigger">Policy</span>
            <span slot="title">Exposure policy</span>
            <span slot="description"
              >Tune what remains visible when the vault is inspected under pressure.</span
            >
            <div class="drawer-demo-form">
              <label class="drawer-demo-field">
                <span>Visible profile</span>
                <select id="drawer-demo-profile">
                  <option>Travel vault</option>
                  <option>Work vault</option>
                  <option>Research vault</option>
                </select>
              </label>
              <label class="drawer-demo-toggle">
                <input type="checkbox" checked />
                <span>
                  <strong>Require device proof</strong>
                  <small>Challenge paired hardware before revealing protected records.</small>
                </span>
              </label>
              <label class="drawer-demo-toggle">
                <input type="checkbox" />
                <span>
                  <strong>Keep decoy activity visible</strong>
                  <small>Leave routine changes available for low-risk review paths.</small>
                </span>
              </label>
            </div>
            <div slot="footer">
              <cv-button variant="ghost" data-drawer-close="drawer-demo-policy">Cancel</cv-button>
              <cv-button id="drawer-demo-save" variant="primary" data-drawer-close="drawer-demo-policy">
                Save policy
              </cv-button>
            </div>
          </cv-drawer>

          <cv-drawer
            id="drawer-demo-session"
            class="drawer-demo-drawer drawer-demo-drawer--sheet"
            placement="bottom"
            drag-to-close
          >
            <span slot="trigger">Session sheet</span>
            <span slot="title">Session details</span>
            <span slot="description">Bottom placement is useful for mobile-adjacent task review.</span>
            <dl class="drawer-demo-detail-list">
              <div>
                <dt>Active route</dt>
                <dd>Vault / Records</dd>
              </div>
              <div>
                <dt>Scroll lock</dt>
                <dd>Enabled while modal</dd>
              </div>
              <div>
                <dt>Dismissal</dt>
                <dd>Escape, backdrop, header close, or touch drag</dd>
              </div>
            </dl>
            <div slot="footer">
              <cv-button variant="primary" data-drawer-close="drawer-demo-session">Done</cv-button>
            </div>
          </cv-drawer>

          <cv-drawer
            id="drawer-demo-notice"
            class="drawer-demo-drawer drawer-demo-drawer--notice"
            placement="top"
          >
            <span slot="trigger">Non-modal notice</span>
            <span slot="title">Sync window</span>
            <p>This top drawer is opened as a non-modal notice so the page remains reachable.</p>
            <div slot="footer">
              <cv-button variant="primary" data-drawer-close="drawer-demo-notice">Acknowledge</cv-button>
            </div>
          </cv-drawer>

          <cv-drawer id="drawer-demo-alert" class="drawer-demo-drawer" type="alertdialog" placement="end">
            <span slot="trigger">Alert drawer</span>
            <span slot="title">Drop visible profile?</span>
            <span slot="description"
              >This removes the selected profile from the current visible surface.</span
            >
            <p>Use alertdialog only when the drawer asks for a high-risk decision.</p>
            <div slot="footer">
              <cv-button variant="ghost" data-drawer-close="drawer-demo-alert">Cancel</cv-button>
              <cv-button variant="danger" data-drawer-close="drawer-demo-alert">Drop profile</cv-button>
            </div>
          </cv-drawer>
        </div>
      </header>

      <section class="drawer-demo-summary" aria-label="Current vault state">
        <div class="drawer-demo-summary-card drawer-demo-summary-card--primary">
          <span class="drawer-demo-label">Primary surface</span>
          <strong>31 visible records</strong>
          <p>
            Operational content stays in place while drawers handle navigation, policy, and contextual review.
          </p>
        </div>
        <div class="drawer-demo-summary-card">
          <span class="drawer-demo-label">Trust boundary</span>
          <strong>Hardware proof required</strong>
          <p>Panels can carry focused controls without turning the route into a dense settings page.</p>
        </div>
      </section>
    </main>
  </section>
</div>

<script>
  document.querySelectorAll('.drawer-demo-shell[data-demo="drawer"]:not([data-ready])').forEach((shell) => {
    shell.dataset.ready = 'true'
    const drawers = new Map([...shell.querySelectorAll('cv-drawer[id]')].map((drawer) => [drawer.id, drawer]))
    const notice = drawers.get('drawer-demo-notice')
    if (notice) notice.modal = false

    shell.querySelectorAll('[data-drawer-close]').forEach((control) => {
      control.addEventListener('click', () => {
        const drawer = drawers.get(control.dataset.drawerClose)
        if (drawer) drawer.open = false
      })
    })
  })
</script>

Anatomy

<cv-drawer> (host)
├── <button part="trigger">
│   └── <slot name="trigger">
└── <div part="overlay"> (hidden when closed)
    └── <section part="panel" role="dialog|alertdialog" data-placement="...">
        ├── <header part="header">
        │   ├── <h2 part="title" id="...">
        │   │   └── <slot name="title">
        │   ├── <p part="description" id="...">
        │   │   └── <slot name="description">
        │   └── <button part="header-close" aria-label="Close">
        │       └── <slot name="header-close">
        ├── <div part="body">
        │   └── <slot>
        └── <footer part="footer">
            └── <slot name="footer">

Attributes

AttributeTypeDefaultDescription
openBooleanfalseWhether the drawer is visible
modalBooleantrueEnables modal behavior (focus trap, scroll lock, backdrop)
placementString"end"Edge the drawer slides from: start | end | top | bottom
typeString"dialog"ARIA role type: dialog | alertdialog
close-on-escapeBooleantrueWhether Escape key closes the drawer
close-on-outside-pointerBooleantrueWhether clicking outside closes the drawer
close-on-outside-focusBooleantrueWhether focusing outside closes the drawer
initial-focus-idString---Id of element to focus when drawer opens
no-headerBooleanfalseHides the header (title, description, header close button)
drag-to-closeBooleanfalseEnables touch drag dismissal in the drawer's closing direction

Slots

SlotDescription
(default)Drawer body content
triggerContent for the trigger button
titleDrawer title text
descriptionDescription text below the title
header-closeIcon content for the header close button (defaults to X)
footerFooter content (action buttons, etc.)

CSS Parts

PartElementDescription
trigger<button>Trigger button that opens the drawer
overlay<div>Backdrop/overlay container
panel<section>Drawer panel with role="dialog" or role="alertdialog"
header<header>Header area containing title, description, and close button
title<h2>Drawer title element
description<p>Drawer description element
header-close<button>Header close icon button
body<div>Body content area
footer<footer>Footer area for user-provided action buttons

CSS Custom Properties

PropertyDefaultDescription
--cv-drawer-z-index40Z-index of the overlay layer
--cv-drawer-size360pxInline size (for start/end) or block size (for top/bottom) of the panel
--cv-drawer-max-sizecalc(100dvh - 32px)Maximum size before internal scrolling (block axis for top/bottom, inline axis for start/end)
--cv-drawer-header-spacingvar(--cv-space-4, 16px)Header padding
--cv-drawer-body-spacingvar(--cv-space-4, 16px)Body padding
--cv-drawer-footer-spacingvar(--cv-space-4, 16px)Footer padding
--cv-drawer-overlay-colorvar(--cv-color-overlay)Backdrop overlay color
--cv-drawer-overlay-transition-duration0msOverlay opacity transition duration
--cv-drawer-overlay-closed-opacity1Overlay opacity while the panel is animating out or before the panel animates in
--cv-drawer-border-radiusvar(--cv-radius-lg, 14px)Panel border radius (applied to the inward edge only)
--cv-drawer-transition-duration250msSlide transition duration
--cv-drawer-drag-offset-x0pxInternal horizontal drag offset applied while touch dragging
--cv-drawer-drag-offset-y0pxInternal vertical drag offset applied while touch dragging
--cv-drawer-drag-overlay-opacity1Internal overlay opacity applied while touch dragging

Visual States

Host selectorDescription
:host([open])Drawer visible, overlay shown
:host([modal])Modal mode active (focus trap, scroll lock, backdrop)
:host([type="alertdialog"])Alert dialog mode with role="alertdialog"
:host([no-header])Header section hidden
:host([placement="start"])Panel anchored to the inline-start edge (left in LTR, right in RTL)
:host([placement="end"])Panel anchored to the inline-end edge (right in LTR, left in RTL)
:host([placement="top"])Panel anchored to the top edge
:host([placement="bottom"])Panel anchored to the bottom edge
:host([drag-to-close])Touch drag dismissal is enabled

Placement layout rules

  • start / end: Panel stretches full viewport block size; inline size set by --cv-drawer-size. Border radius applied to the inner vertical edge.
  • top / bottom: Panel stretches full viewport inline size; block size set by --cv-drawer-size. Border radius applied to the inner horizontal edge.
  • start and end follow CSS logical directions and automatically flip in RTL layouts.

Events

EventDetailDescription
cv-input{open: boolean}Fires when open state changes via user interaction
cv-change{open: boolean}Fires when open state commits
cv-show---Fires when drawer begins to open
cv-after-show---Fires after drawer open animation completes
cv-hide---Fires when drawer begins to close
cv-after-hide---Fires after drawer close animation completes

cv-input and cv-change fire only for user-initiated state changes (trigger click, Escape, outside pointer, outside focus, header close). Programmatic open attribute changes do not emit these events.

Reactive State Mapping

cv-drawer is a visual adapter over headless createDrawer.

UIKit PropertyDirectionHeadless Binding
openattr -> actionactions.open() / actions.close()
modalattr -> optionpassed as isModal in createDrawer(options)
placementattr -> actionactions.setPlacement(placement) and passed as placement in createDrawer(options)
typeattr -> optionpassed as type in createDrawer(options)
close-on-escapeattr -> optionpassed as closeOnEscape in createDrawer(options)
close-on-outside-pointerattr -> optionpassed as closeOnOutsidePointer in createDrawer(options)
close-on-outside-focusattr -> optionpassed as closeOnOutsideFocus in createDrawer(options)
initial-focus-idattr -> optionpassed as initialFocusId in createDrawer(options)
no-headerattr -> DOMcontrols header visibility (UIKit-only, no headless binding)
Headless StateDirectionDOM Reflection
state.isOpen()state -> attr[open] host attribute
state.isModal()state -> attr[modal] host attribute
state.type()state -> attr[type] host attribute
state.placement()state -> attr[placement] host attribute
state.isFocusTrapped()state -> effectactivates focus trap within the drawer
state.shouldLockScroll()state -> effectapplies overflow: hidden to document.body
state.restoreTargetId()state -> effectfocuses the trigger element on close
state.initialFocusTargetId()state -> effectfocuses the specified element on open
  • contracts.getTriggerProps() is spread onto [part="trigger"] to apply role, aria-haspopup, aria-expanded, aria-controls, tabindex, and click/keydown handlers.
  • contracts.getOverlayProps() is spread onto [part="overlay"] to apply hidden, data-open, and outside pointer/focus handlers.
  • contracts.getPanelProps() is spread onto [part="panel"] to apply role (dialog or alertdialog), aria-modal, aria-labelledby, aria-describedby, data-placement, tabindex, and keydown handler.
  • contracts.getTitleProps() is spread onto [part="title"] to apply the id for aria-labelledby.
  • contracts.getDescriptionProps() is spread onto [part="description"] to apply the id for aria-describedby.
  • contracts.getHeaderCloseButtonProps() is spread onto [part="header-close"] to apply role, tabindex, aria-label: 'Close', and click handler.
  • UIKit dispatches cv-input and cv-change events by observing isOpen changes triggered by user interaction (not by controlled open attribute changes).
  • UIKit dispatches cv-show/cv-after-show/cv-hide/cv-after-hide lifecycle events to bracket CSS transitions.
  • UIKit owns scroll lock implementation, focus trap implementation, focus restoration, backdrop rendering, slide animations, and CSS transitions -- headless provides signals, UIKit applies side effects.

ChromVoid UIKit documentation