All components

Bottom Directional Drawer

Bottom Directional Drawer is a React and Tailwind CSS overlay component from UI Layouts, licensed MIT. Copy it and paste it into your project.

UI Layouts MIT overlays

What it is

Bottom Directional Drawer is a React and Tailwind CSS overlay component from UI Layouts, licensed MIT. Copy it and paste it into your project.

How to use it

Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.

Open in the catalogue

Five free copies a month. No card.

Needs

npm i motion lucide-react

Licence

This component comes from UI Layouts and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.

Bottom Directional Drawer

./components/ui/directional-drawer.tsx
'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>;
}
./registry/components/drawer/bottom-directional-drawer.tsx
'use client';
import {
  DirectionalDrawer,
  DrawerContent,
  DrawerTrigger,
} from '@/components/ui/directional-drawer';
import { useMediaQuery } from '@/hooks/use-media-query';
import { Edit, X } from 'lucide-react';
import { motion } from 'motion/react';
import Image from 'next/image';
import { useState } from 'react';
import { Drawer } from 'vaul';
export default function BottomDirectionalDrawer() {
  const [sidebarOpen, setSidebarOpen] = useState(false);
  return (
    <>
      <DirectionalDrawer
        open={sidebarOpen}
        setOpen={setSidebarOpen}
        direction={'bottom'}
        outsideClose={true}
      >
        <DrawerContent className='w-full h-full flex justify-end'>
          <div className='p-5 rounded-t-md grow w-full pt-14'>
            <h1 className='font-medium  text-2xl'>Update Profile Image</h1>
            <p className='text-sm text-muted-foreground'>
              Upload a new profile image or remove the current one.
            </p>
            <div className='p-2 space-y-4 '>
              <span className='relative flex justify-center overflow-hidden rounded-xl w-full '>
                <span className='grid place-content-center h-40  w-40 rounded-xl dark:bg-neutral-800 bg-muted'>
                  JP
                </span>
              </span>
              <div className='mb-3'>
                <input
                  className='w-full border file:p-2 file:bg-black  file:border-none  file:text-white rounded-xs overflow-hidden'
                  type='file'
                  id='formFile'
                />
              </div>
              <button
                type='submit'
                className='w-full rounded-xs dark:bg-white bg-black  p-2 dark:text-black text-white'
              >
                Submit
              </button>
            </div>
          </div>
        </DrawerContent>
      </DirectionalDrawer>
      <div className='flex justify-center'>
        <figure className='h-96 w-96 relative'>
          <Image
            src={'/myself.webp'}
            width={600}
            height={600}
            className='h-full w-full object-cover rounded-lg '
            alt='profile_image'
          />
          <motion.button
            whileTap={{ scale: 0.8 }}
            onClick={() => setSidebarOpen(true)}
            className='absolute left-2 bottom-2 p-4 dark:bg-black bg-white rounded-lg shadow-black'
          >
            <Edit />
          </motion.button>
        </figure>
      </div>
    </>
  );
}

Similar components