DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Drawer

An accessible slide-in panel built on the Base UI Drawer primitive. Compound component pattern with Header, Body, Footer, Title, Description, and Item rows. Swipe-to-dismiss on touch and pointer, popover surface, four sides, five sizes, five corner-rounding options, edge or floating inset presentation, and a full modal spectrum (modal, focus-trap-only, non-modal). LTR and RTL support.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import {
  Drawer,
  DrawerTrigger,
  DrawerClose,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerDescription,
  DrawerBody,
  DrawerFooter,
} from "@tessinaui/ui";
<Drawer>
  <DrawerTrigger className="...">Open</DrawerTrigger>

  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Panel title</DrawerTitle>
      <DrawerDescription>Supporting description text.</DrawerDescription>
    </DrawerHeader>

    <DrawerBody>
      <p>Content goes here.</p>
    </DrawerBody>

    <DrawerFooter>
      <DrawerClose className="...">Cancel</DrawerClose>
      <DrawerClose className="...">Save</DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Examples

Default

A trigger that opens a right-side panel with a header, body, and footer.

Sides

Slide the panel in from the right, left, top, or bottom edge.

Sizes

Five widths for left/right panels — sm, md, lg, xl, and full.

Rounded

Corner-rounding cascades from the root rounded prop to the panel's inner edge, the close button, and the drag handle.

Curving your own content

Content you put inside — a tile or thumbnail in the body — should curve like the chrome around it, or it reads as pasted on. The drawer panel publishes its inner radius as a CSS variable:

<img className="rounded-[var(--drawer-item-radius)]" />

It tracks whatever the component is actually doing: the explicit step when rounded is set (8px at md), and 0 at rounded="none" — so your content squares off exactly when the container does.

Inset

inset floats the panel off the viewport edges with a safe-area-aware gap and rounds every corner — the desktop floating-panel presentation.

Scrim

Three background treatments while the drawer is open: blur frosts the page (default), dim is a plain dark overlay, and shadow keeps the page fully visible with the panel separated by elevation alone.

Non-modal

modal={false} removes the scrim and scroll lock so the page stays interactive — the inspector-panel pattern. modal="trap-focus" keeps keyboard focus inside without locking the page.

Header actions

The header's action slot renders a trailing control between the title and the close button, and the back slot renders a leading DrawerBack for multi-step flows.

Nested navigation

Drill into sub-lists inside one drawer — DrawerBack appears in the header while a section is open.

Bottom Sheet

A bottom-anchored sheet with a stacked footer and a live drag handle — swipe down (touch) or drag the handle (mouse) to dismiss.

Form in a bottom sheet

On a phone, focusing a field opens the software keyboard. Every side="bottom" drawer handles it through Base UI's Drawer.VirtualKeyboardProvider: the sheet grows by the keyboard's height so its content sits above the keyboard, and the focused field scrolls into view. There is nothing to wire up.

For a layout of your own, the keyboard's height is published as --drawer-keyboard-inset on the drawer's viewport. Always read it with the fallback — var(--drawer-keyboard-inset, 0px) — because it is unset while no keyboard is up. Do not set interactive-widget=resizes-content in the viewport meta tag: the provider measures the keyboard from the visual viewport, and that setting shrinks the layout viewport instead.

Top panel

A top-anchored panel with a tab bar under the header and a full-width call to action — the search-sheet pattern.

Navigation

A left-side navigation drawer built from DrawerItem rows — hover, focus, active, disabled, and selected states, a destructive sign-out row, and radii that follow the drawer's rounded.

Profile header

A hero header block with an avatar, grouped menu rows, and a pinned bottom row for settings.

Menu

A narrow, headerless action menu — name the dialog with aria-label on DrawerContent — with a sign-out row and a version/legal footer block.

Filter panel

Accordion filter sections with radio and checkbox groups, a results meta line, and paired Clear/Apply actions pinned below the scrolling body. Swap the primary label for a live count ("Show 260 results") when the query runs as filters change.

Form

A create-form drawer with labeled fields, a toggle row, and a wizard-style footer — leading back button, trailing Cancel/Submit.

Details

A record-details panel with a full-bleed media hero and metadata rows, plus an empty-state variant.

Snap points

A bottom sheet that rests at preset heights. Drag it between them, or activate the grabber to step through — a swipe has no keyboard equivalent, so with snap points the grabber becomes a real button and is the keyboard path to the detents.

Swipe area

An invisible edge strip that opens the drawer when swiped inward. Opt-in, and rendered as a sibling of DrawerContent because it has to exist while the drawer is closed. Always keep a trigger as well: the strip is aria-hidden and unfocusable by design, and on iOS a left-edge strip competes with the system back gesture, which usually wins.

API Reference

Drawer

Root component. Wraps the Base UI Drawer primitive and provides context for side, size, rounded, inset, modal, and dir. Swiping toward the drawer's edge dismisses it: touch swipes work anywhere on the panel (scrollable content is respected — the gesture only takes over at the scroll edge), and mouse drags work from the drag handle, so text inside the panel stays selectable.

PropTypeDefaultDescription
side"right" | "left" | "top" | "bottom""right"Which edge the panel slides from
size"sm" | "md" | "lg" | "xl" | "full""md"Panel width (right/left) or max-height (top/bottom)
rounded"none" | "sm" | "md" | "lg" | "full""md"Corner rounding. Edge drawers round the inner corners; inset drawers round every corner. Cascades to the handle, close button, and skeleton
insetbooleanfalseFloat the panel off the viewport edges with a safe-area-aware gap and all-corner rounding
dir"ltr" | "rtl""ltr"Text direction applied to the panel and its children
openboolean—Controlled open state
defaultOpenboolean—Uncontrolled initial open state
onOpenChange(open: boolean, event: Event) => void—Called when the open state changes
onOpenChangeComplete(open: boolean) => void—Called after the open/close animation settles
dismissiblebooleantrueWhether backdrop click, Escape, and the swipe gesture close the drawer — false keeps only explicit closes
modalboolean | "trap-focus"truetrue traps focus, locks scroll, and shows the scrim. "trap-focus" traps focus only — no scrim, no scroll lock. false is fully non-modal: the page stays interactive
scrim"blur" | "dim" | "shadow""blur"Background treatment while open (modal only): frosted blur, plain dark overlay, or no overlay with the panel elevated by shadow — outside clicks dismiss in all three
snapPoints(number | string)[]—Heights a side="bottom" sheet snaps to. 0–1 is a fraction of the viewport, above 1 is pixels, strings carry their own px / rem unit. Declaring them also makes the grabber operable
snapPointnumber | string | null—The active snap point. Pair with onSnapPointChange to control it
defaultSnapPointnumber | string | nullfirst snap pointThe initial snap point when uncontrolled
onSnapPointChange(snapPoint) => void—Called when the active snap point changes
snapToSequentialPointsbooleanfalseDisable velocity-based skipping, so drag distance alone picks the next point

DrawerContent

Renders the panel inside a portal. Includes the backdrop automatically when the drawer is modal; modal="trap-focus" and modal={false} render no scrim.

PropTypeDefaultDescription
sideDrawerSidefrom contextOverride the slide-in side
sizeDrawerSizefrom contextOverride the panel size
roundedDrawerRoundedfrom contextOverride the corner rounding
insetbooleanfrom contextOverride the floating presentation
showHandlebooleantrue for bottom, false otherwiseShow the drag handle pill
handleLabelstring"Resize drawer"Accessible name for the grabber when the drawer has snap points

The grabber has two shapes, decided by whether snap points exist rather than by a prop. Without them it is an ornament — aria-hidden, unfocusable, because the panel is already draggable and a control that only duplicates a gesture is noise in the tab order. With them it is a real 44px button: activating it advances to the next height and wraps at the end, which is also the only way a keyboard or switch user reaches the detents.

Size values:

Sizeright / lefttop / bottom
sm320pxmax-h 40dvh
md400pxmax-h 60dvh
lg512pxmax-h 75dvh
xl640pxmax-h 90dvh
full100%100dvh

DrawerHeader

PropTypeDefaultDescription
iconReactNode—Leading icon rendered beside the title
backReactNode—Leading back affordance rendered before the icon/title — typically <DrawerBack aria-label="Back" />
actionReactNode—Trailing action cluster between the title and the close button (the Reset / Clear / Done pattern)
showClosebooleantrueWhether to render the built-in X close button

The first child of DrawerHeader is treated as the title row (shares a line with the back slot, icon, action slot, and close button). All subsequent children (e.g. DrawerDescription) render full-width below it.

DrawerBack

Leading back button for multi-step or drill-down drawers. Renders an icon-only button that mirrors the close button's ergonomics — ghost, xs, radius from the drawer's rounded — and flips its arrow automatically in RTL. aria-label is required.

PropTypeDefaultDescription
aria-labelstringrequiredAccessible name for the icon-only button
…restIconButton propsvariant="ghost", size="xs"Everything except icon is forwarded

DrawerTitle

Renders as Dialog.Title. Accepts all <div> props plus className.

DrawerDescription

Renders as Dialog.Description. Accepts all <div> props plus className.

DrawerBody

Scrollable content area. Grows to fill available space between header and footer. Accepts all <div> props plus className.

DrawerFooter

Action area. For side="bottom" and side="top" drawers, buttons stack vertically (full-width). For side="left" and side="right" drawers, buttons align horizontally to the end. Pass className="flex-row items-center justify-end" to override the auto-stacking behaviour.

DrawerItem

Menu/navigation row for drawer content: icon + label + trailing cluster with hover, focus-visible, active, disabled, and selected states, a 44px touch floor on phones, and a radius one step inside the drawer's rounded. Activating a row closes the drawer unless closeOnSelect={false} (drill-down rows).

PropTypeDefaultDescription
iconReactNode—Leading icon (decorative — the row text is the accessible name)
trailingReactNode—Trailing cluster: chevron, shortcut, badge
intent"default" | "destructive""default"Destructive renders the sign-out / delete treatment
selectedbooleanfalseMarks the current row (aria-current) with the filled treatment
closeOnSelectbooleantrueClose the drawer when the row activates

DrawerTrigger

Wraps the primitive's Trigger (adds data-slot="drawer-trigger"). Renders a <button> that opens the drawer. Use the render prop to swap in a custom element.

DrawerClose

Wraps the primitive's Close (adds data-slot="drawer-close"). Renders a <button> that closes the drawer. Use the render prop to swap in a custom element.

DrawerBackdrop

Pre-styled backdrop with blur and fade animation. Included automatically by DrawerContent while the drawer is modal — never add it as a sibling of DrawerContent.

Accessibility

  • The panel is a Base UI dialog: role="dialog", labeled by DrawerTitle and described by DrawerDescription. Headerless drawers must pass aria-label on DrawerContent.
  • Focus moves into the panel on open, is trapped while modal, and returns to the trigger on close. Escape closes when dismissible.
  • Every part exposes a stable data-slot attribute (drawer-content, drawer-header, drawer-body, drawer-footer, drawer-back, …) for testing and agent tooling.
  • Enter/exit runs a 450ms deceleration curve; releasing a swipe scales the exit duration by the flick velocity, so a hard flick lands fast. All of it is disabled under prefers-reduced-motion: reduce. The swipe gesture itself is direct manipulation — the panel follows the pointer 1:1 — and remains available under reduced motion.
  • Swipe-to-dismiss respects scrollable content (the gesture engages at the scroll edge), skips range inputs and multi-touch, and never fights mouse text selection inside the panel body.

DrawerSwipeArea

An invisible edge strip that opens the drawer when swiped inward. Render it as a sibling of DrawerContent — it has to exist while the drawer is closed.

PropTypeDefaultDescription
widthnumber24Thickness of the grab strip in px

DrawerProvider · DrawerIndent · DrawerIndentBackground

Opt-in page treatment: the app scales back slightly while a drawer is open, so a sheet reads as sitting in front of the page rather than pasted onto it. Wrap the app once in DrawerProvider, put the app inside DrawerIndent, and put DrawerIndentBackground immediately before it. The scale is suppressed under prefers-reduced-motion.

<DrawerProvider>
  <DrawerIndentBackground />
  <DrawerIndent>
    {/* your app */}
  </DrawerIndent>
</DrawerProvider>
DividerDropdown Menu

On this page

PlaygroundInstallationUsageExamplesDefaultSidesSizesRoundedCurving your own contentInsetScrimNon-modalHeader actionsNested navigationBottom SheetForm in a bottom sheetTop panelNavigationProfile headerMenuFilter panelFormDetailsSnap pointsSwipe areaAPI ReferenceDrawerDrawerContentDrawerHeaderDrawerBackDrawerTitleDrawerDescriptionDrawerBodyDrawerFooterDrawerItemDrawerTriggerDrawerCloseDrawerBackdropAccessibilityDrawerSwipeAreaDrawerProvider · DrawerIndent · DrawerIndentBackground