my/ui

Command Palette

Search for a command to run...

All components

Directional Drawer

modals

Ui-Layouts component.

responsive · 640px

Install

Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:

$npx shadcn@latest add https://your-domain/r/directional-drawer.json

Usage

"use client";
import {
  DirectionalDrawer,
  DrawerContent,
  DrawerTrigger,
} from "@/registry/ui-layouts/directional-drawer";

export default function Demo() {
  return (
    <div className="flex items-center justify-center min-h-[400px] bg-background">
      <DirectionalDrawer direction="left" outsideClose={true}>
        <DrawerTrigger>
          <button className="px-6 py-3 rounded-md bg-foreground text-background font-medium hover:opacity-90 transition-opacity">
            Open Drawer
          </button>
        </DrawerTrigger>
        <DrawerContent className="p-8 pt-12 flex flex-col gap-6 h-full">
          <div>
            <h2 className="text-xl font-semibold mb-1">Navigation</h2>
            <p className="text-sm text-muted-foreground">
              Slide-in panel from the left side of the screen.
            </p>
          </div>
          <nav className="flex flex-col gap-2">
            {["Dashboard", "Projects", "Settings", "Profile"].map((item) => (
              <a
                key={item}
                href="#"
                className="px-4 py-2 rounded-md hover:bg-muted text-sm font-medium transition-colors"
              >
                {item}
              </a>
            ))}
          </nav>
          <div className="mt-auto">
            <button className="w-full px-4 py-2 rounded-md bg-foreground text-background text-sm font-medium hover:opacity-90 transition-opacity">
              Get Started
            </button>
          </div>
        </DrawerContent>
      </DirectionalDrawer>
    </div>
  );
}

Component source

'use client';
import { cn } from '@/lib/utils';
import { X } from 'lucide-react';
import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
import { Drawer as VaulSidebar } from 'vaul';

interface DrawerContextProps {
  open: boolean;
  setOpen: (open: boolean) => void;
}

const DrawerContext = createContext<DrawerContextProps | undefined>(undefined);

export const useDirectionalDrawer = () => {
  const context = useContext(DrawerContext);
  if (!context) {
    throw new Error('useDirectionalDrawer must be used within a DirectionalDrawer');
  }
  return context;
};

interface DirectionalDrawerProps {
  children: ReactNode;
  open?: boolean;
  setOpen?: (open: boolean) => void;
  direction?: 'left' | 'right' | 'top' | 'bottom';
  outsideClose?: boolean;
  className?: string;
}

export function DirectionalDrawer({
  children,
  open: controlledOpen,
  setOpen: controlledSetOpen,
  direction = 'left',
  outsideClose = true,
  className,
}: DirectionalDrawerProps) {
  const [internalOpen, setInternalOpen] = useState(false);
  const [isDesktop, setIsDesktop] = useState(false);

  const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
  const setOpen = controlledSetOpen || setInternalOpen;

  useEffect(() => {
    const mediaQuery = window.matchMedia('(min-width: 768px)');
    const handleMediaChange = (event: MediaQueryListEvent) => {
      setIsDesktop(event.matches);
    };

    setIsDesktop(mediaQuery.matches);
    mediaQuery.addEventListener('change', handleMediaChange);

    return () => {
      mediaQuery.removeEventListener('change', handleMediaChange);
    };
  }, []);

  const trigger = React.Children.toArray(children).find(
    (child: any) => child.type === DrawerTrigger
  );
  const content = React.Children.toArray(children).filter(
    (child: any) => child.type !== DrawerTrigger
  );

  // Helper function to get positioning and sizing classes
  const getDirectionClasses = () => {
    switch (direction) {
      case 'right':
        return {
          position: 'right-0 bottom-0',
          size: outsideClose ? 'sm:w-[450px] w-[90%] h-full' : 'w-full h-full',
          border: 'border-l',
          handlePosition: 'top-[40%] left-2',
          handleSize: 'h-16 w-[0.30rem]',
        };
      case 'top':
        return {
          position: 'top-0 left-0',
          size: outsideClose ? 'w-full sm:h-[450px] h-[90%]' : 'w-full h-full',
          border: 'border-b',
          handlePosition: 'bottom-2 left-[40%]',
          handleSize: 'w-16 h-[0.30rem]',
        };
      case 'bottom':
        return {
          position: 'bottom-0 left-0',
          size: outsideClose ? 'w-full sm:h-[450px] h-[90%]' : 'w-full h-full',
          border: 'border-t',
          handlePosition: 'top-2 left-[40%]',
          handleSize: 'w-16 h-[0.30rem]',
        };
      case 'left':
      default:
        return {
          position: 'left-0 bottom-0',
          size: outsideClose ? 'sm:w-[450px] w-[90%] h-full' : 'w-full h-full',
          border: 'border-r',
          handlePosition: 'top-[40%] right-2',
          handleSize: 'h-16 w-[0.30rem]',
        };
    }
  };

  const directionClasses = getDirectionClasses();
  const vaulDirection =
    direction === 'right'
      ? 'right'
      : direction === 'top'
        ? 'top'
        : direction === 'bottom'
          ? 'bottom'
          : 'left';

  return (
    <DrawerContext.Provider value={{ open, setOpen }}>
      {trigger}

      <VaulSidebar.Root
        open={open}
        direction={vaulDirection}
        onOpenChange={setOpen}
        dismissible={isDesktop ? false : true}
      >
        <VaulSidebar.Portal>
          <VaulSidebar.Overlay
            className='fixed inset-0 dark:bg-black/40 bg-white/50 backdrop-blur-xs z-50'
            onClick={() => setOpen(false)}
          />
          <VaulSidebar.Content
            className={cn(
              `${directionClasses.border} z-50 ${directionClasses.size} fixed ${directionClasses.position} ${
                outsideClose ? 'dark:bg-zinc-950 bg-zinc-100' : ''
              }`,
              className
            )}
          >
            <div
              className={`${
                outsideClose
                  ? 'w-full h-full'
                  : `dark:bg-neutral-900 relative bg-white ${directionClasses.border} ${directionClasses.size}`
              }`}
            >
              {isDesktop ? (
                <button
                  className='flex justify-end w-full absolute right-2 top-2'
                  onClick={() => setOpen(false)}
                >
                  <X />
                </button>
              ) : (
                <div
                  className={`absolute ${directionClasses.handlePosition} mx-auto ${directionClasses.handleSize} shrink-0 rounded-full bg-neutral-600 my-4`}
                />
              )}
              {content}
            </div>
          </VaulSidebar.Content>
        </VaulSidebar.Portal>
      </VaulSidebar.Root>
    </DrawerContext.Provider>
  );
}

export function DrawerContent({
  children,
  className,
}: {
  children: ReactNode;
  className?: string;
}) {
  return <div className={cn('', className)}>{children}</div>;
}

export function DrawerTrigger({ children }: { children: ReactNode }) {
  const { setOpen } = useDirectionalDrawer();
  return <div onClick={() => setOpen(true)}>{children}</div>;
}

Dependencies

motionlucide-react

Source: Ui-Layouts