Kosorikōsori alpha

Drawer

A drawer component for React.

Dependencies

Installation

Install the primitive

Install the vaul package.

npm install vaul

Copy-paste the component

Copy and paste the component code in a .tsx file.

'use client';
 
import { forwardRef } from 'react';
import { clsx } from 'clsx/lite';
import { tv } from 'tailwind-variants';
import { Drawer as DrawerPrimitive } from 'vaul';
 
const drawerStyles = tv({
  slots: {
    overlay: 'fixed inset-0 z-50 bg-black-a6',
    content:
      'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border border-grey-line bg-grey-base',
    contentHandle: 'bg-grey-bg-hover mx-auto mt-4 h-2 w-[100px] rounded-full',
    header: clsx('grid gap-1.5 p-4 text-center', 'sm:text-left'),
    title: 'text-lg font-semibold leading-none tracking-tight',
    description: 'text-sm text-grey-text',
    footer: 'mt-auto flex flex-col gap-2 p-4',
  },
});
 
const { overlay, content, contentHandle, header, title, description, footer } =
  drawerStyles();
 
type DrawerProps = React.ComponentProps<typeof DrawerPrimitive.Root>;
 
/**
 * Drawer component that provides a sliding panel for displaying content.
 *
 * @param {DrawerProps} props - The props for the Drawer component.
 * @param {boolean} [props.shouldScaleBackground=true] - Whether to scale the background when the drawer is open.
 *
 * @example
 * <Drawer>
 *   <DrawerTrigger>Open</DrawerTrigger>
 *   <DrawerContent>
 *     <DrawerHeader>
 *       <DrawerTitle>Are you absolutely sure?</DrawerTitle>
 *       <DrawerDescription>This action cannot be undone.</DrawerDescription>
 *     </DrawerHeader>
 *     <DrawerFooter>
 *       <Button>Submit</Button>
 *       <DrawerClose>
 *         <Button variant='outline'>Cancel</Button>
 *       </DrawerClose>
 *     </DrawerFooter>
 *   </DrawerContent>
 * </Drawer>
 *
 * @see {@link https://dub.sh/ui-drawer Drawer Docs} for further information.
 */
export const Drawer = ({
  shouldScaleBackground = true,
  ...props
}: DrawerProps) => (
  <DrawerPrimitive.Root
    shouldScaleBackground={shouldScaleBackground}
    {...props}
  />
);
 
Drawer.displayName = 'Drawer';
 
export const DrawerTrigger = DrawerPrimitive.Trigger;
 
export const DrawerPortal = DrawerPrimitive.Portal;
 
type DrawerOverlayRef = React.ElementRef<typeof DrawerPrimitive.Overlay>;
type DrawerOverlayProps = React.ComponentPropsWithoutRef<
  typeof DrawerPrimitive.Overlay
>;
 
/**
 * DrawerOverlay component that covers the background when the Drawer is open.
 *
 * @param {DrawerOverlayProps} props - The props for the DrawerOverlay component.
 *
 * @example
 * <DrawerOverlay />
 */
export const DrawerOverlay = forwardRef<DrawerOverlayRef, DrawerOverlayProps>(
  ({ className, ...props }, ref) => (
    <DrawerPrimitive.Overlay
      ref={ref}
      className={overlay({ className })}
      {...props}
    />
  ),
);
 
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
 
type DrawerContentRef = React.ElementRef<typeof DrawerPrimitive.Content>;
type DrawerContentProps = React.ComponentPropsWithoutRef<
  typeof DrawerPrimitive.Content
>;
 
/**
 * DrawerContent component that wraps the content of the Drawer.
 *
 * @param {DrawerContentProps} props - The props for the DrawerContent component.
 * @param {React.ReactNode} props.children - The content to be rendered inside the DrawerContent.
 *
 * @example
 * <DrawerContent>
 *   <p>Your content goes here.</p>
 * </DrawerContent>
 */
export const DrawerContent = forwardRef<DrawerContentRef, DrawerContentProps>(
  ({ className, children, ...props }, ref) => (
    <DrawerPortal>
      <DrawerOverlay />
      <DrawerPrimitive.Content
        ref={ref}
        className={content({ className })}
        {...props}
      >
        <div className={contentHandle()} />
        {children}
      </DrawerPrimitive.Content>
    </DrawerPortal>
  ),
);
 
DrawerContent.displayName = 'DrawerContent';
 
type DrawerHeaderProps = React.HTMLAttributes<HTMLDivElement>;
 
/**
 * DrawerHeader component that wraps the header content of the Drawer.
 *
 * @param {DrawerHeaderProps} props - The props for the DrawerHeader component.
 *
 * @example
 * <DrawerHeader>
 *   <DrawerTitle>Drawer Title</DrawerTitle>
 * </DrawerHeader>
 */
export const DrawerHeader = ({ className, ...props }: DrawerHeaderProps) => (
  <div className={header({ className })} {...props} />
);
 
DrawerHeader.displayName = 'DrawerHeader';
 
type DrawerTitleRef = React.ElementRef<typeof DrawerPrimitive.Title>;
type DrawerTitleProps = React.ComponentPropsWithoutRef<
  typeof DrawerPrimitive.Title
>;
 
/**
 * DrawerTitle component that displays the title of the Drawer.
 *
 * @param {DrawerTitleProps} props - The props for the DrawerTitle component.
 *
 * @example
 * <DrawerTitle>Drawer Title</DrawerTitle>
 */
export const DrawerTitle = forwardRef<DrawerTitleRef, DrawerTitleProps>(
  ({ className, ...props }, ref) => (
    <DrawerPrimitive.Title
      ref={ref}
      className={title({ className })}
      {...props}
    />
  ),
);
 
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
 
type DrawerDescriptionRef = React.ElementRef<
  typeof DrawerPrimitive.Description
>;
type DrawerDescriptionProps = React.ComponentPropsWithoutRef<
  typeof DrawerPrimitive.Description
>;
 
/**
 * DrawerDescription component that provides a description for the Drawer.
 *
 * @param {DrawerDescriptionProps} props - The props for the DrawerDescription component.
 *
 * @example
 * <DrawerDescription>Drawer Description</DrawerDescription>
 */
export const DrawerDescription = forwardRef<
  DrawerDescriptionRef,
  DrawerDescriptionProps
>(({ className, ...props }, ref) => (
  <DrawerPrimitive.Description
    ref={ref}
    className={description({ className })}
    {...props}
  />
));
 
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
 
type DrawerFooterProps = React.HTMLAttributes<HTMLDivElement>;
 
/**
 * DrawerFooter component that wraps the footer content of the Drawer.
 *
 * @param {DrawerFooterProps} props - The props for the DrawerFooter component.
 *
 * @example
 * <DrawerFooter>
 *   <Button>Close</Button>
 * </DrawerFooter>
 */
export const DrawerFooter = ({ className, ...props }: DrawerFooterProps) => (
  <div className={footer({ className })} {...props} />
);
 
DrawerFooter.displayName = 'DrawerFooter';
 
export const DrawerClose = DrawerPrimitive.Close;

Update import paths

Update the @kosori/ui import paths to fit your project structure, for example, using ~/components/ui.

Usage

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from '~/components/ui/drawer';
<Drawer>
  <DrawerTrigger>Open</DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Are you absolutely sure?</DrawerTitle>
      <DrawerDescription>This action cannot be undone.</DrawerDescription>
    </DrawerHeader>
    <DrawerFooter>
      <Button>Submit</Button>
      <DrawerClose>
        <Button variant='outline'>Cancel</Button>
      </DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Examples

Responsive

You can combine the <Dialog /> and <Drawer /> components to create a responsive dialog. This renders a <Dialog /> component on desktop and a <Drawer /> on mobile.

On this page