fab uifab ui

Drawer

A panel that slides in from the edge of the screen with swipe-to-dismiss gestures.

'use client';

import { Button } from '@/components/ui/button';
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer';

export function DrawerDefault() {
  return (
    <Drawer>
      <DrawerTrigger render={<Button variant='outline' />}>
        Open Drawer
      </DrawerTrigger>
      <DrawerPopup>
        <DrawerContent>
          <DrawerHeader>
            <DrawerTitle>Notifications</DrawerTitle>
            <DrawerDescription>
              You are all caught up. Good job!
            </DrawerDescription>
          </DrawerHeader>
          <DrawerFooter>
            <DrawerClose render={<Button variant='outline' />}>
              Close
            </DrawerClose>
          </DrawerFooter>
        </DrawerContent>
      </DrawerPopup>
    </Drawer>
  );
}

Installation

pnpm dlx shadcn@latest add @fab-ui/drawer

Usage

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
<Drawer>
  <DrawerTrigger>Open</DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Drawer Title</DrawerTitle>
      <DrawerDescription>Drawer description goes here.</DrawerDescription>
    </DrawerHeader>
    <DrawerFooter>
      <DrawerClose>Close</DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Examples

Right Side

Use swipeDirection="right" on the Drawer component to slide the panel in from the right.

'use client';

import { Button } from '@/components/ui/button';
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer';

export function DrawerRight() {
  return (
    <Drawer swipeDirection='right'>
      <DrawerTrigger render={<Button variant='outline' />}>
        Open Drawer
      </DrawerTrigger>
      <DrawerPopup>
        <DrawerContent>
          <DrawerHeader>
            <DrawerTitle>Drawer</DrawerTitle>
            <DrawerDescription>
              This is a drawer that slides in from the side. You can swipe to
              dismiss it.
            </DrawerDescription>
          </DrawerHeader>
          <DrawerFooter>
            <DrawerClose render={<Button variant='outline' />}>
              Close
            </DrawerClose>
          </DrawerFooter>
        </DrawerContent>
      </DrawerPopup>
    </Drawer>
  );
}

Non-Modal

Set modal={false} and disablePointerDismissal on the Drawer to keep the page interactive while the drawer is open.

'use client';

import { Button } from '@/components/ui/button';
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer';

export function DrawerNonModal() {
  return (
    <Drawer swipeDirection='right' modal={false} disablePointerDismissal>
      <DrawerTrigger render={<Button variant='outline' />}>
        Open Non-Modal Drawer
      </DrawerTrigger>
      <DrawerPopup>
        <DrawerContent>
          <DrawerHeader>
            <DrawerTitle>Non-modal Drawer</DrawerTitle>
            <DrawerDescription>
              This drawer does not trap focus and ignores outside clicks. Use
              the close button or swipe to dismiss it.
            </DrawerDescription>
          </DrawerHeader>
          <DrawerFooter>
            <DrawerClose render={<Button variant='outline' />}>
              Close
            </DrawerClose>
          </DrawerFooter>
        </DrawerContent>
      </DrawerPopup>
    </Drawer>
  );
}

Nested Drawers

Nest multiple Drawer components and manage each with controlled open / onOpenChange state to create a stacking effect.

'use client';

import * as React from 'react';

import { Button } from '@/components/ui/button';
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer';

export function DrawerNested() {
  const [firstOpen, setFirstOpen] = React.useState(false);
  const [secondOpen, setSecondOpen] = React.useState(false);
  const [thirdOpen, setThirdOpen] = React.useState(false);

  return (
    <Drawer
      open={firstOpen}
      onOpenChange={(nextOpen) => {
        setFirstOpen(nextOpen);
        if (!nextOpen) {
          setSecondOpen(false);
          setThirdOpen(false);
        }
      }}
    >
      <DrawerTrigger render={<Button variant='outline' />}>
        Open Drawer Stack
      </DrawerTrigger>
      <DrawerPopup>
        <DrawerContent>
          <DrawerTitle className='mb-1'>Account</DrawerTitle>
          <DrawerDescription className='mb-6'>
            Nested drawers can be styled to stack, while each drawer remains
            independently focus managed.
          </DrawerDescription>

          <div className='flex items-center justify-end gap-4'>
            <div className='mr-auto'>
              <Drawer
                open={secondOpen}
                onOpenChange={(nextOpen) => {
                  setSecondOpen(nextOpen);
                  if (!nextOpen) {
                    setThirdOpen(false);
                  }
                }}
              >
                <DrawerTrigger
                  render={<Button variant='link' className='px-0' />}
                >
                  Security settings
                </DrawerTrigger>
                <DrawerPopup>
                  <DrawerContent>
                    <DrawerTitle className='mb-1'>Security</DrawerTitle>
                    <DrawerDescription className='mb-6'>
                      Review sign-in activity and update your security
                      preferences.
                    </DrawerDescription>

                    <ul className='mb-6 list-disc pl-5 text-muted-foreground'>
                      <li>Passkeys enabled</li>
                      <li>2FA via authenticator app</li>
                      <li>3 signed-in devices</li>
                    </ul>

                    <div className='flex items-center justify-end gap-4'>
                      <div className='mr-auto'>
                        <Drawer open={thirdOpen} onOpenChange={setThirdOpen}>
                          <DrawerTrigger
                            render={<Button variant='link' className='px-0' />}
                          >
                            Advanced options
                          </DrawerTrigger>
                          <DrawerPopup>
                            <DrawerContent>
                              <DrawerTitle className='mb-1'>
                                Advanced
                              </DrawerTitle>
                              <DrawerDescription className='mb-6'>
                                This drawer is taller to demonstrate
                                variable-height stacking.
                              </DrawerDescription>

                              <div className='mb-4 grid gap-1.5'>
                                <label
                                  className='text-sm font-medium'
                                  htmlFor='device-name'
                                >
                                  Device name
                                </label>
                                <input
                                  id='device-name'
                                  className='w-full rounded-md border border-input bg-background px-2.5 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
                                  defaultValue='Personal laptop'
                                />
                              </div>

                              <div className='mb-6 grid gap-1.5'>
                                <label
                                  className='text-sm font-medium'
                                  htmlFor='notes'
                                >
                                  Notes
                                </label>
                                <textarea
                                  id='notes'
                                  className='w-full resize-y rounded-md border border-input bg-background px-2.5 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
                                  defaultValue='Rotate recovery codes and revoke older sessions.'
                                  rows={3}
                                />
                              </div>

                              <div className='flex justify-end'>
                                <DrawerClose
                                  render={<Button variant='outline' />}
                                >
                                  Done
                                </DrawerClose>
                              </div>
                            </DrawerContent>
                          </DrawerPopup>
                        </Drawer>
                      </div>

                      <DrawerClose render={<Button variant='outline' />}>
                        Close
                      </DrawerClose>
                    </div>
                  </DrawerContent>
                </DrawerPopup>
              </Drawer>
            </div>

            <DrawerClose render={<Button variant='outline' />}>
              Close
            </DrawerClose>
          </div>
        </DrawerContent>
      </DrawerPopup>
    </Drawer>
  );
}

Snap Points

Pass a snapPoints array to Drawer and use unstyled on DrawerPopup to apply custom popup styling for snap behavior.

'use client';

import * as React from 'react';

import { Button } from '@/components/ui/button';
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer';

const TOP_MARGIN_REM = 1;
const VISIBLE_SNAP_POINTS_REM = [30];

function toViewportSnapPoint(heightRem: number) {
  return `${heightRem + TOP_MARGIN_REM}rem`;
}

const snapPoints = [...VISIBLE_SNAP_POINTS_REM.map(toViewportSnapPoint), 1];

export function DrawerSnapPoints() {
  return (
    <Drawer snapPoints={snapPoints}>
      <DrawerTrigger render={<Button variant='outline' />}>
        Open Snap Drawer
      </DrawerTrigger>
      <DrawerPopup
        unstyled
        className='relative flex max-h-[calc(100dvh-var(--top-margin))] min-h-0 w-full transform-[translateY(calc(var(--drawer-snap-point-offset)+var(--drawer-swipe-movement-y)))] touch-none flex-col overflow-visible rounded-t-xl bg-background pb-[max(0px,calc(var(--drawer-snap-point-offset)+var(--drawer-swipe-movement-y)))] text-foreground shadow-[0_-16px_48px_rgb(0_0_0/0.12),0_6px_18px_rgb(0_0_0/0.06)] ring-1 ring-foreground/10 transition-[transform,box-shadow] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] outline-none [--bleed:3rem] after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-(--bleed) after:bg-background after:content-[""] data-ending-style:transform-[translateY(100%)] data-ending-style:pb-0 data-ending-style:shadow-[0_-16px_48px_rgb(0_0_0/0),0_6px_18px_rgb(0_0_0/0)] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*300ms)] data-starting-style:transform-[translateY(100%)] data-starting-style:pb-0 data-starting-style:shadow-[0_-16px_48px_rgb(0_0_0/0),0_6px_18px_rgb(0_0_0/0)] data-swiping:transition-none data-swiping:select-none'
        style={
          { '--top-margin': `${TOP_MARGIN_REM}rem` } as React.CSSProperties
        }
      >
        <div className='shrink-0 touch-none border-b border-border px-6 pt-3.5 pb-3'>
          <div className='mx-auto h-1 w-12 rounded-full bg-muted-foreground/30' />
          <DrawerTitle className='mt-2.5 cursor-default'>
            Snap points
          </DrawerTitle>
        </div>
        <DrawerContent className='min-h-0 flex-1 touch-auto overflow-y-auto overscroll-contain px-6 pt-4 pb-[calc(1.5rem+env(safe-area-inset-bottom,0px))]'>
          <div className='mx-auto w-full max-w-87.5'>
            <DrawerDescription className='mb-4'>
              Drag the sheet to snap between a compact peek and a near
              full-height view.
            </DrawerDescription>
            <div className='mb-6 grid gap-3' aria-hidden>
              {Array.from({ length: 20 }, (_, index) => (
                <div
                  key={index}
                  className='h-12 rounded-xl border border-border bg-muted'
                />
              ))}
            </div>
            <div className='flex items-center justify-end gap-4'>
              <DrawerClose render={<Button variant='outline' />}>
                Close
              </DrawerClose>
            </div>
          </div>
        </DrawerContent>
      </DrawerPopup>
    </Drawer>
  );
}

Action Sheet

Use DrawerPopup with unstyled and overlayClassName to customize the popup and overlay styling for an action sheet layout.

'use client';

import * as React from 'react';

import { Button } from '@/components/ui/button';
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerPopup,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer';

const ACTIONS = [
  'Unfollow',
  'Mute',
  'Add to Favourites',
  'Add to Close Friends',
  'Restrict',
];

export function DrawerActionSheet() {
  const [open, setOpen] = React.useState(false);

  return (
    <Drawer open={open} onOpenChange={setOpen}>
      <DrawerTrigger render={<Button variant='outline' />}>
        Open Action Sheet
      </DrawerTrigger>
      <DrawerPopup
        unstyled
        overlayClassName='bg-black/40'
        className='pointer-events-none relative flex w-full max-w-md transform-[translateY(var(--drawer-swipe-movement-y))] touch-auto flex-col gap-3 overflow-y-auto overscroll-contain bg-transparent px-4 pt-4 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] text-foreground ring-0 transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] outline-none data-ending-style:transform-[translateY(calc(100%+1rem))] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*300ms)] data-starting-style:transform-[translateY(calc(100%+1rem))] data-swiping:transition-none data-swiping:select-none'
      >
        <DrawerContent className='pointer-events-auto overflow-hidden rounded-2xl bg-background ring-1 ring-foreground/10'>
          <DrawerTitle className='sr-only'>Profile actions</DrawerTitle>
          <DrawerDescription className='sr-only'>
            Choose an action for this user.
          </DrawerDescription>

          <ul
            className='m-0 list-none divide-y divide-foreground/10 p-0'
            aria-label='Profile actions'
          >
            {ACTIONS.map((action, index) => (
              <li key={action}>
                {index === 0 && (
                  <DrawerClose className='sr-only'>
                    Close action sheet
                  </DrawerClose>
                )}
                <button
                  type='button'
                  className='block w-full border-0 bg-transparent px-5 py-4 text-center text-sm select-none hover:bg-muted focus-visible:bg-muted focus-visible:outline-none'
                  onClick={() => setOpen(false)}
                >
                  {action}
                </button>
              </li>
            ))}
          </ul>
        </DrawerContent>
        <div className='pointer-events-auto overflow-hidden rounded-2xl bg-background ring-1 ring-foreground/10'>
          <button
            type='button'
            className='block w-full border-0 bg-transparent px-5 py-4 text-center text-sm text-destructive select-none hover:bg-muted focus-visible:bg-muted focus-visible:outline-none'
            onClick={() => setOpen(false)}
          >
            Block User
          </button>
        </div>
      </DrawerPopup>
    </Drawer>
  );
}