Left Directional Drawer
Left 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
Left 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.
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.
Left Directional Drawer
'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>;
}
'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 LeftDirectionalDrawer() {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<>
<DirectionalDrawer
open={sidebarOpen}
setOpen={setSidebarOpen}
direction={'left'}
outsideClose={true}
>
<DrawerContent>
<figure className='flex flex-col w-full h-full rounded-xl overflow-hidden'>
{/* Header / Body */}
<div className='p-6 flex flex-col gap-4 grow'>
<div>
<h1 className='font-semibold text-xl'>Update Profile Image</h1>
<p className='text-sm text-muted-foreground mt-1'>
Upload a new profile image or remove the current one.
</p>
</div>
{/* Avatar Preview */}
<div className='flex justify-center'>
<div className='h-32 w-32 rounded-xl dark:bg-neutral-800 bg-muted grid place-content-center text-xl font-medium'>
JP
</div>
</div>
{/* File Input */}
<div>
<label className='text-sm font-medium mb-1 block'>Profile Picture</label>
<input
type='file'
id='formFile'
className='w-full bg-background border border-border rounded-md p-2 file:bg-black file:text-white file:border-none file:px-3 file:py-1 cursor-pointer'
/>
</div>
{/* Submit */}
<button
type='submit'
className='w-full rounded-md bg-black text-white dark:bg-white dark:text-black p-2 font-medium'
>
Save Changes
</button>
</div>
</figure>
</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>
</>
);
}