gnome-ui
Components

Drawer

A panel that slides in from the edge of the screen.

Drawer component
import { DrawerPreview as Drawer } from 'gnome-ui/drawer';
import { PanelRight, Settings, Bell, User, Lock, Wifi, ChevronRight, X, SlidersHorizontal, Moon, Shield } from 'lucide-react';

export function DrawerDefault() {
  return (
    <Drawer.Root swipeDirection="right">
      <Drawer.Trigger className="inline-flex items-center justify-center gap-2 rounded-xl text-sm font-medium leading-none transition-colors duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 h-9 border border-border bg-card px-4 text-foreground hover:bg-accent">
        <Icons.PanelRight className="size-4 shrink-0" />
        Open drawer
      </Drawer.Trigger>

      <Drawer.Portal>
        <Drawer.Backdrop className="fixed inset-0 min-h-dvh bg-black/40 backdrop-blur-sm transition-all duration-300 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 supports-[-webkit-touch-callout:none]:absolute" />
        <Drawer.Viewport className="fixed inset-0 flex justify-end">
          <Drawer.Popup className="flex h-full w-80 flex-col border-l border-border bg-card shadow-2xl outline-none transition-transform duration-300 ease-out data-[ending-style]:translate-x-full data-[starting-style]:translate-x-full">
            <Drawer.Content className="flex flex-1 flex-col overflow-y-auto">
              <div className="flex items-center gap-3 border-b border-border px-5 py-4">
                <Icons.Settings className="size-4 shrink-0 text-primary" />
                <Drawer.Title className="flex-1 text-base font-semibold text-foreground">
                  Quick settings
                </Drawer.Title>
                <Drawer.Close className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring">
                  <Icons.X className="size-4" />
                </Drawer.Close>
              </div>

              <nav className="flex flex-col divide-y divide-border">
                {[
                  { icon: Icons.Wifi,              label: 'Network',    value: 'Connected' },
                  { icon: Icons.Bell,              label: 'Notifications', value: 'On'     },
                  { icon: Icons.Moon,              label: 'Night Light',value: 'Off'       },
                  { icon: Icons.Shield,            label: 'Privacy',    value: 'Managed'   },
                  { icon: Icons.SlidersHorizontal, label: 'Display',    value: 'Auto'      },
                  { icon: Icons.User,              label: 'Accounts',   value: '2 linked'  },
                  { icon: Icons.Lock,              label: 'Security',   value: 'Active'    },
                ].map(({ icon: Icon, label, value }) => (
                  <button
                    key={label}
                    className="flex w-full items-center gap-3 px-5 py-3 text-left transition-colors duration-150 hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
                  >
                    <Icon className="size-4 shrink-0 text-muted-foreground" />
                    <span className="flex-1 text-sm text-foreground">{label}</span>
                    <span className="text-xs text-muted-foreground">{value}</span>
                    <Icons.ChevronRight className="size-3.5 shrink-0 text-muted-foreground" />
                  </button>
                ))}
              </nav>
            </Drawer.Content>

            <div className="border-t border-border px-5 py-3">
              <Drawer.Close className="inline-flex items-center justify-center gap-2 rounded-xl text-sm font-medium leading-none transition-colors duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 h-9 w-full bg-primary text-primary-foreground hover:brightness-95">
                Close
              </Drawer.Close>
            </div>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

Anatomy

import { DrawerPreview as Drawer } from 'gnome-ui/drawer';

<Drawer.Provider>
  <Drawer.IndentBackground />
  <Drawer.Indent>
    <Drawer.Root>
      <Drawer.Trigger />
      <Drawer.Portal>
        <Drawer.Backdrop />
        <Drawer.Viewport>
          <Drawer.Popup>
            <Drawer.Content>
              <Drawer.Title />
              <Drawer.Description />
              <Drawer.Close />
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  </Drawer.Indent>
</Drawer.Provider>

Examples

Snap points

A bottom drawer that snaps to 40% or full height, inspired by GNOME Mobile action sheets. Drag up to reveal all filter options.

Drawer component
import { DrawerPreview as Drawer } from 'gnome-ui/drawer';
import { SlidersHorizontal, X } from 'lucide-react';

const filterGroups = [
  { label: 'Type',     options: ['All', 'Documents', 'Images', 'Videos', 'Audio'] },
  { label: 'Modified', options: ['Any time', 'Today', 'This week', 'This month']  },
  { label: 'Owner',    options: ['Anyone', 'Me', 'Others']                        },
];

export function DrawerSnapPoints() {
  return (
    <Drawer.Root snapPoints={[0.4, 1]} defaultSnapPoint={0.4} swipeDirection="down">
      <Drawer.Trigger className="inline-flex items-center justify-center gap-2 rounded-xl text-sm font-medium leading-none transition-colors duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 h-9 border border-border bg-card px-4 text-foreground hover:bg-accent">
        <Icons.SlidersHorizontal className="size-4 shrink-0" />
        Filter results
      </Drawer.Trigger>

      <Drawer.Portal>
        <Drawer.Backdrop className="fixed inset-0 min-h-dvh bg-black/40 backdrop-blur-sm transition-all duration-300 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 supports-[-webkit-touch-callout:none]:absolute" />
        <Drawer.Viewport className="fixed inset-0 flex items-end">
          <Drawer.Popup className="flex max-h-full w-full flex-col rounded-t-2xl border-t border-border bg-card shadow-2xl outline-none transition-transform duration-300 ease-out data-[ending-style]:translate-y-full data-[starting-style]:translate-y-full">
            <div className="flex justify-center pb-1 pt-3">
              <div className="h-1 w-10 rounded-full bg-border" />
            </div>

            <Drawer.Content className="flex flex-1 flex-col overflow-y-auto px-5 pb-6">
              <div className="flex items-center gap-3 py-4">
                <Icons.SlidersHorizontal className="size-4 shrink-0 text-primary" />
                <Drawer.Title className="flex-1 text-base font-semibold text-foreground">
                  Filter results
                </Drawer.Title>
                <Drawer.Close className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors duration-150 hover:bg-accent hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring">
                  <Icons.X className="size-4" />
                </Drawer.Close>
              </div>

              <Drawer.Description className="mb-5 text-sm leading-relaxed text-muted-foreground">
                Drag up to see all filter options. Swipe down to dismiss.
              </Drawer.Description>

              {filterGroups.map(({ label, options }) => (
                <div key={label} className="mb-5">
                  <p className="mb-2 text-xs font-semibold uppercase tracking-widest text-muted-foreground">
                    {label}
                  </p>
                  <div className="flex flex-wrap gap-2">
                    {options.map((opt, i) => (
                      <button
                        key={opt}
                        className={`rounded-xl border px-3 py-1.5 text-xs font-medium transition-colors duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring ${
                          i === 0
                            ? 'border-primary bg-primary/10 text-primary'
                            : 'border-border bg-card text-foreground hover:bg-accent'
                        }`}
                      >
                        {opt}
                      </button>
                    ))}
                  </div>
                </div>
              ))}

              <Drawer.Close className="inline-flex items-center justify-center gap-2 rounded-xl text-sm font-medium leading-none transition-colors duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 mt-2 h-10 w-full bg-primary text-primary-foreground hover:brightness-95">
                Apply filters
              </Drawer.Close>
            </Drawer.Content>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

A left-side navigation drawer using swipeDirection="left", inspired by GNOME Files places panel.

Drawer component
import { DrawerPreview as Drawer } from 'gnome-ui/drawer';
import { PanelRight, User, Lock, Bell, Shield, Moon, X } from 'lucide-react';

const navItems = [
  { icon: Icons.User,   label: 'Home',      active: true  },
  { icon: Icons.Lock,   label: 'Documents', active: false },
  { icon: Icons.Bell,   label: 'Downloads', active: false },
  { icon: Icons.Shield, label: 'Pictures',  active: false },
  { icon: Icons.Moon,   label: 'Music',     active: false },
];

export function DrawerNavigation() {
  return (
    <Drawer.Root swipeDirection="left">
      <Drawer.Trigger className="inline-flex items-center justify-center gap-2 rounded-xl text-sm font-medium leading-none transition-colors duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 h-9 border border-border bg-card px-4 text-foreground hover:bg-accent">
        <Icons.PanelRight className="size-4 shrink-0 scale-x-[-1]" />
        Navigation
      </Drawer.Trigger>

      <Drawer.Portal>
        <Drawer.Backdrop className="fixed inset-0 min-h-dvh bg-black/40 backdrop-blur-sm transition-all duration-300 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 supports-[-webkit-touch-callout:none]:absolute" />
        <Drawer.Viewport className="fixed inset-0 flex justify-start">
          <Drawer.Popup className="flex h-full w-72 flex-col border-r border-border bg-sidebar shadow-2xl outline-none transition-transform duration-300 ease-out data-[ending-style]:-translate-x-full data-[starting-style]:-translate-x-full">
            <Drawer.Content className="flex flex-1 flex-col overflow-y-auto">
              <div className="flex items-center gap-3 border-b border-sidebar-border px-4 py-4">
                <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-semibold">
                  U
                </div>
                <div className="flex-1 min-w-0">
                  <p className="text-sm font-semibold text-sidebar-foreground truncate">ubuntu</p>
                  <p className="text-xs text-muted-foreground truncate">/home/ubuntu</p>
                </div>
                <Drawer.Close className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors duration-150 hover:bg-sidebar-accent hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring">
                  <Icons.X className="size-3.5" />
                </Drawer.Close>
              </div>

              <div className="px-3 pb-1 pt-3">
                <p className="text-[11px] font-semibold uppercase tracking-widest text-muted-foreground">Places</p>
              </div>

              <nav className="flex flex-col px-2">
                {navItems.map(({ icon: Icon, label, active }) => (
                  <button
                    key={label}
                    className={`flex w-full items-center gap-2.5 rounded-xl px-3 py-2.5 text-left text-sm transition-colors duration-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring ${
                      active
                        ? 'bg-sidebar-accent font-semibold text-sidebar-primary'
                        : 'text-sidebar-foreground hover:bg-sidebar-accent'
                    }`}
                  >
                    <Icon className={`size-4 shrink-0 ${active ? 'text-sidebar-primary' : 'text-muted-foreground'}`} />
                    {label}
                  </button>
                ))}
              </nav>
            </Drawer.Content>

            <Drawer.Description className="border-t border-sidebar-border px-4 py-3 text-xs text-muted-foreground">
              Swipe left to close.
            </Drawer.Description>
          </Drawer.Popup>
        </Drawer.Viewport>
      </Drawer.Portal>
    </Drawer.Root>
  );
}

DrawerBackdrop

An overlay displayed beneath the popup. Renders a &lt;div&gt; element.

Documentation: Base UI Drawer

API reference

Provider

Provides a shared context for coordinating global Drawer UI, such as indent/background effects based on whether any Drawer is open.

Provider Props:

PropTypeDefaultDescription
childrenReactNode--

IndentBackground

An element placed before <Drawer.Indent> to render a background layer that can be styled based on whether any drawer is open.

IndentBackground Props:

PropTypeDefaultDescription
classNamestring | ((state: Drawer.IndentBackground.State) => string | undefined)-CSS class applied to the element, or a function that returns a class based on the component’s state.
styleCSSProperties | ((state: Drawer.IndentBackground.State) => CSSProperties | undefined)--
renderReactElement | ((props: HTMLProps, state: Drawer.IndentBackground.State) => ReactElement)-Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.

Indent

A wrapper element intended to contain your app's main UI. Applies data-active when any drawer within the nearest <Drawer.Provider> is open.

Indent Props:

PropTypeDefaultDescription
classNamestring | ((state: Drawer.Indent.State) => string | undefined)-CSS class applied to the element, or a function that returns a class based on the component’s state.
styleCSSProperties | ((state: Drawer.Indent.State) => CSSProperties | undefined)--
renderReactElement | ((props: HTMLProps, state: Drawer.Indent.State) => ReactElement)-Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.

Root

Groups all parts of the drawer. Doesn't render its own HTML element.

Root Props:

PropTypeDefaultDescription
defaultOpenbooleanfalseWhether the drawer is initially open.To render a controlled drawer, use the open prop instead.
openboolean-Whether the drawer is currently open.
onOpenChange((open: boolean, eventDetails: Drawer.Root.ChangeEventDetails) => void)-Event handler called when the drawer is opened or closed.
snapPointsDrawerSnapPoint[]-Snap points used to position the drawer. Use numbers between 0 and 1 to represent fractions of the viewport height, numbers greater than 1 as pixel values, or strings in px/rem units (for example, '148px' or '30rem').
defaultSnapPointDrawerSnapPoint | null-The initial snap point value when uncontrolled.
snapPointDrawerSnapPoint | null-The currently active snap point. Use with onSnapPointChange to control the snap point.
onSnapPointChange((snapPoint: DrawerSnapPoint | null, eventDetails: Drawer.Root.SnapPointChangeEventDetails) => void)-Callback fired when the snap point changes.
actionsRefRefObject<Drawer.Root.Actions | null>-A ref to imperative actions.* unmount: When specified, the drawer will not be unmounted when closed. Instead, the unmount function must be called to unmount the drawer manually. Useful when the drawer's animation is controlled by an external library.
* close: Closes the drawer imperatively when called.
defaultTriggerIdstring | null-ID of the trigger that the drawer is associated with. This is useful in conjunction with the defaultOpen prop to create an initially open drawer.
disablePointerDismissalbooleanfalseDetermines whether the drawer should close on outside clicks.
handleDrawer.Handle<Payload>-A handle to associate the drawer with a trigger. If specified, allows detached triggers to control the drawer's open state. Can be created with the Drawer.createHandle() method.
modalboolean | 'trap-focus'trueDetermines if the drawer enters a modal state when open.* true: user interaction is limited to just the drawer: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled.
  • false: user interaction with the rest of the document is allowed.
  • 'trap-focus': focus is trapped inside the drawer, but document page scroll is not locked and pointer interactions outside of it remain enabled. | | onOpenChangeComplete | ((open: boolean) => void) | - | Event handler called after any animations complete when the drawer is opened or closed. | | snapToSequentialPoints | boolean | false | Disables velocity-based snap skipping so drag distance determines the next snap point. | | swipeDirection | DrawerSwipeDirection | 'down' | The swipe direction used to dismiss the drawer. | | triggerId | string \| null | - | ID of the trigger that the drawer is associated with. This is useful in conjunction with the open prop to create a controlled drawer. There's no need to specify this prop when the drawer is uncontrolled (i.e. when the open prop is not set). | | children | ReactNode \| PayloadChildRenderFunction<Payload> | - | The content of the drawer. |

Trigger

A button that opens the drawer. Renders a <button> element.

Trigger Props:

PropTypeDefaultDescription
handleDrawerHandle<Payload>-A handle to associate the trigger with a drawer. Can be created with the Drawer.createHandle() method.
nativeButtonbooleantrueWhether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (e.g. <div>).
payloadPayload-A payload to pass to the drawer when it is opened.
idstring-ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for drawers in controlled mode (with the Drawer.Root triggerId prop).
classNamestring | ((state: Drawer.Trigger.State) => string | undefined)-CSS class applied to the element, or a function that returns a class based on the component’s state.
styleCSSProperties | ((state: Drawer.Trigger.State) => CSSProperties | undefined)--
renderReactElement | ((props: HTMLProps, state: Drawer.Trigger.State) => ReactElement)-Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.

Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div> element.

Portal Props:

PropTypeDefaultDescription
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null-A parent element to render the portal element into.
classNamestring | ((state: Drawer.Portal.State) => string | undefined)-CSS class applied to the element, or a function that returns a class based on the component’s state.
styleCSSProperties | ((state: Drawer.Portal.State) => CSSProperties | undefined)--
keepMountedbooleanfalseWhether to keep the portal mounted in the DOM while the popup is hidden.
renderReactElement | ((props: HTMLProps, state: Drawer.Portal.State) => ReactElement)-Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.

Backdrop

An overlay displayed beneath the popup. Renders a <div> element.

Backdrop Props:

PropTypeDefaultDescription
forceRenderbooleanfalseWhether the backdrop is forced to render even when nested.
classNamestring | ((state: Drawer.Backdrop.State) => string | undefined)-CSS class applied to the element, or a function that returns a class based on the component’s state.
styleCSSProperties | ((state: Drawer.Backdrop.State) => CSSProperties | undefined)--
renderReactElement | ((props: HTMLProps, state: Drawer.Backdrop.State) => ReactElement)-Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.

Backdrop CSS Variables:

VariableTypeDefaultDescription
--drawer-swipe-progressnumber-The swipe progress of the drawer gesture.

Viewport

A positioning container for the drawer popup that can be made scrollable. Renders a <div> element.

Viewport Props:

PropTypeDefaultDescription
classNamestring | ((state: Drawer.Viewport.State) => string | undefined)-CSS class applied to the element, or a function that returns a class based on the component’s state.
styleCSSProperties | ((state: Drawer.Viewport.State) => CSSProperties | undefined)--
renderReactElement | ((props: HTMLProps, state: Drawer.Viewport.State) => ReactElement)-Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.

A container for the drawer contents. Renders a <div> element.

Popup Props:

PropTypeDefaultDescription
initialFocusboolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)-Determines the element to focus when the drawer is opened.* false: Do not move focus.
  • true: Move focus based on the default behavior (first tabbable element or popup).
  • RefObject: Move focus to the ref element.
  • function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, or false/undefined to do nothing. | | finalFocus | boolean \| RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => boolean \| void \| HTMLElement \| null) | - | Determines the element to focus when the drawer is closed.* false: Do not move focus.
  • true: Move focus based on the default behavior (trigger or previously focused element).
  • RefObject: Move focus to the ref element.
  • function: Called with the interaction type (mouse, touch, pen, or keyboard). Return an element to focus, true to use the default behavior, or false/undefined to do nothing. | | className | string \| ((state: Drawer.Popup.State) => string \| undefined) | - | CSS class applied to the element, or a function that returns a class based on the component’s state. | | style | CSSProperties \| ((state: Drawer.Popup.State) => CSSProperties \| undefined) | - | - | | render | ReactElement \| ((props: HTMLProps, state: Drawer.Popup.State) => ReactElement) | - | Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render. |

Popup Data Attributes:

AttributeTypeDescription
data-expanded-Present when the drawer is at the expanded (full-height) snap point.
data-nested-drawer-open-Present when a nested drawer is open.
data-nested-drawer-swiping-Present when a nested drawer is being swiped.
data-swipe-direction'up' | 'down' | 'left' | 'right'Indicates the swipe direction.
data-swipe-dismiss-Present when the drawer is dismissed by swiping.
data-swiping-Present when the drawer is being swiped.

Popup CSS Variables:

VariableTypeDefaultDescription
--drawer-frontmost-heightCSS-The height of the frontmost open drawer in the current nested drawer stack.
--drawer-heightCSS-The height of the drawer popup.
--drawer-snap-point-offsetCSS-The snap point offset used for translating the drawer.
--drawer-swipe-movement-xCSS-The swipe movement on the X axis.
--drawer-swipe-movement-yCSS-The swipe movement on the Y axis.
--drawer-swipe-strengthnumber-A scalar (0.1-1) used to scale the swipe release transition duration in CSS.
--nested-drawersnumber-The number of nested drawers that are currently open.

Title

A heading that labels the drawer. Renders an <h2> element.

Title Props:

PropTypeDefaultDescription
classNamestring | ((state: Drawer.Title.State) => string | undefined)-CSS class applied to the element, or a function that returns a class based on the component’s state.
styleCSSProperties | ((state: Drawer.Title.State) => CSSProperties | undefined)--
renderReactElement | ((props: HTMLProps, state: Drawer.Title.State) => ReactElement)-Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.

Description

A paragraph with additional information about the drawer. Renders a <p> element.

Description Props:

PropTypeDefaultDescription
classNamestring | ((state: Drawer.Description.State) => string | undefined)-CSS class applied to the element, or a function that returns a class based on the component’s state.
styleCSSProperties | ((state: Drawer.Description.State) => CSSProperties | undefined)--
renderReactElement | ((props: HTMLProps, state: Drawer.Description.State) => ReactElement)-Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.

Close

A button that closes the drawer. Renders a <button> element.

Close Props:

PropTypeDefaultDescription
nativeButtonbooleantrueWhether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (e.g. <div>).
classNamestring | ((state: Drawer.Close.State) => string | undefined)-CSS class applied to the element, or a function that returns a class based on the component’s state.
styleCSSProperties | ((state: Drawer.Close.State) => CSSProperties | undefined)--
renderReactElement | ((props: HTMLProps, state: Drawer.Close.State) => ReactElement)-Allows you to replace the component’s HTML element with a different tag, or compose it with another component.Accepts a ReactElement or a function that returns the element to render.

On this page