MC Drawer
A slide-out panel for navigation or content.
Overview
The mc-drawer component is a responsive slide-out panel for navigation, settings, forms, and
secondary workflows. It is built on top of vaul, with MicroClub styling for overlays, side
panels, bottom sheets, nav rows, headers, footers, and accessible close controls.
Use it when content should stay close to the current page without taking the user into a full route change.
Preview
Loading...
'use client';import { Moon } from 'lucide-react';import { McDrawer, McDrawerClose, McDrawerContent, McDrawerDescription, McDrawerHeader, McDrawerNav, McDrawerNavItem, McDrawerTitle, McDrawerTrigger,} from '../ui/mc-drawer';const navItems = [ { label: 'Account', hasArrow: true }, { label: 'Socials', hasArrow: false }, { label: 'Settings', hasArrow: true }, { label: 'Help', hasArrow: true }, { label: 'Info', hasArrow: true },];function DesktopWithIcons() { return ( <McDrawer direction="right"> <McDrawerTrigger className="inline-flex items-center justify-center rounded-full bg-primary px-5 py-3.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"> Drawer — icons </McDrawerTrigger> <McDrawerContent> <McDrawerTitle className="sr-only">Navigation — icons</McDrawerTitle> <McDrawerNav /> <div className="flex flex-1 flex-col gap-0 overflow-y-auto px-0 pt-9"> <McDrawerNavItem icon={<Moon size={16} />} rightElement={<ToggleSwitch />}> Dark mode </McDrawerNavItem> {navItems.map(({ label, hasArrow }) => ( <McDrawerNavItem key={label} icon={<Moon size={16} />} hasArrow={hasArrow}> {label} </McDrawerNavItem> ))} </div> </McDrawerContent> </McDrawer> );}function DesktopNoIcons() { return ( <McDrawer direction="right"> <McDrawerTrigger className="inline-flex items-center justify-center rounded-full bg-primary px-5 py-3.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"> Drawer — no icons </McDrawerTrigger> <McDrawerContent> <McDrawerTitle className="sr-only">Navigation — no icons</McDrawerTitle> <McDrawerNav /> <div className="flex flex-1 flex-col gap-0 overflow-y-auto px-0 pt-9"> <McDrawerNavItem rightElement={<ToggleSwitch />}>Dark mode</McDrawerNavItem> {navItems.map(({ label, hasArrow }) => ( <McDrawerNavItem key={label} hasArrow={hasArrow}> {label} </McDrawerNavItem> ))} </div> </McDrawerContent> </McDrawer> );}function MobileSheet() { return ( <McDrawer direction="bottom"> <McDrawerTrigger className="inline-flex items-center justify-center rounded-full bg-primary px-5 py-3.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"> Drawer — mobile </McDrawerTrigger> <McDrawerContent> <div className="flex flex-col gap-6 px-4 py-6"> <McDrawerHeader className="gap-2 p-0"> <McDrawerTitle>Edit profile</McDrawerTitle> <McDrawerDescription> Make changes to your profile here. Click save when you're done. </McDrawerDescription> </McDrawerHeader> <div className="flex flex-col gap-4"> <div className="flex flex-col gap-1.5"> <label htmlFor="drawer-email" className="text-sm font-medium text-foreground"> Email </label> <input id="drawer-email" type="email" defaultValue="m@example.com" className="w-full rounded-lg border border-border bg-input px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring" /> </div> <div className="flex flex-col gap-1.5"> <label htmlFor="drawer-username" className="text-sm font-medium text-foreground"> Username </label> <input id="drawer-username" type="text" defaultValue="m@example.com" className="w-full rounded-lg border border-border bg-input px-4 py-3 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring" /> </div> </div> <McDrawerClose className="w-full rounded-lg bg-primary py-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"> Save changes </McDrawerClose> </div> </McDrawerContent> </McDrawer> );}export default function DrawerDemo() { return ( <div className="flex flex-wrap gap-4"> <DesktopWithIcons /> <DesktopNoIcons /> <MobileSheet /> </div> );}function ToggleSwitch() { return ( <div role="switch" aria-checked="false" tabIndex={0} className="relative h-6 w-10 shrink-0 cursor-pointer rounded-full bg-muted focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50" > <span className="absolute left-1 top-1 h-4 w-4 rounded-full bg-background shadow-sm transition-transform" /> </div> );}Add to Your Project
Deploy mc-drawer directly to your components/ui directory:
npx mcoli-ui@latest add mc-drawer
pnpm dlx mcoli-ui@latest add mc-drawer
yarn dlx mcoli-ui@latest add mc-drawer
bunx mcoli-ui@latest add mc-drawer
Manual Setup
If you prefer manual control:
Install the core dependencies
npm install vaul
pnpm add vaul
yarn add vaul
bun add vaul
Copy the source code
'use client';import * as React from 'react';import { Drawer as McDrawerPrimitive } from 'vaul';import { cn } from '@/lib/utils';function McDrawer({ ...props }: React.ComponentProps<typeof McDrawerPrimitive.Root>) { return <McDrawerPrimitive.Root data-slot="drawer" {...props} />;}function McDrawerTrigger({ ...props }: React.ComponentProps<typeof McDrawerPrimitive.Trigger>) { return <McDrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;}function McDrawerPortal({ ...props }: React.ComponentProps<typeof McDrawerPrimitive.Portal>) { return <McDrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;}function McDrawerClose({ ...props }: React.ComponentProps<typeof McDrawerPrimitive.Close>) { return <McDrawerPrimitive.Close data-slot="drawer-close" {...props} />;}function McDrawerOverlay({ className, ...props}: React.ComponentProps<typeof McDrawerPrimitive.Overlay>) { return ( <McDrawerPrimitive.Overlay data-slot="drawer-overlay" className={cn( 'fixed inset-0 z-50 bg-foreground/20', 'supports-backdrop-filter:backdrop-blur-xs', 'data-open:animate-in data-open:fade-in-0', 'data-closed:animate-out data-closed:fade-out-0', className )} {...props} /> );}function McDrawerContent({ className, children, ...props}: React.ComponentProps<typeof McDrawerPrimitive.Content>) { return ( <McDrawerPortal> <McDrawerOverlay /> <McDrawerPrimitive.Content data-slot="drawer-content" className={cn( 'group/drawer-content fixed z-50 flex flex-col', 'bg-popover text-popover-foreground', 'shadow-2xl', 'data-[vaul-drawer-direction=bottom]:inset-x-0', 'data-[vaul-drawer-direction=bottom]:bottom-0', 'data-[vaul-drawer-direction=bottom]:max-h-dvh', 'data-[vaul-drawer-direction=bottom]:rounded-t-2xl', 'data-[vaul-drawer-direction=bottom]:border', 'data-[vaul-drawer-direction=bottom]:border-border', 'data-[vaul-drawer-direction=top]:inset-x-0', 'data-[vaul-drawer-direction=top]:top-0', 'data-[vaul-drawer-direction=top]:max-h-dvh', 'data-[vaul-drawer-direction=top]:rounded-b-2xl', 'data-[vaul-drawer-direction=top]:border', 'data-[vaul-drawer-direction=top]:border-border', 'data-[vaul-drawer-direction=left]:inset-y-0', 'data-[vaul-drawer-direction=left]:left-0', 'data-[vaul-drawer-direction=left]:w-drawer', 'data-[vaul-drawer-direction=left]:max-w-full', 'data-[vaul-drawer-direction=left]:rounded-r-2xl', 'data-[vaul-drawer-direction=left]:border', 'data-[vaul-drawer-direction=left]:border-border', 'data-[vaul-drawer-direction=right]:inset-y-0', 'data-[vaul-drawer-direction=right]:right-0', 'data-[vaul-drawer-direction=right]:w-drawer', 'data-[vaul-drawer-direction=right]:max-w-full', 'data-[vaul-drawer-direction=right]:rounded-l-2xl', 'data-[vaul-drawer-direction=right]:border', 'data-[vaul-drawer-direction=right]:border-border', className )} {...props} > <div className="mx-auto mt-4 hidden h-1 w-10 shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block group-data-[vaul-drawer-direction=top]/drawer-content:block" /> {children} </McDrawerPrimitive.Content> </McDrawerPortal> );}function McDrawerHeader({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="drawer-header" className={cn('flex flex-col gap-2 px-4 pt-0 pb-0', className)} {...props} /> );}function McDrawerFooter({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="drawer-footer" className={cn('mt-auto flex flex-col gap-2 p-4 pt-0', className)} {...props} /> );}function McDrawerTitle({ className, ...props}: React.ComponentProps<typeof McDrawerPrimitive.Title>) { return ( <McDrawerPrimitive.Title data-slot="drawer-title" className={cn('text-base font-semibold text-foreground leading-snug', className)} {...props} /> );}function McDrawerDescription({ className, ...props}: React.ComponentProps<typeof McDrawerPrimitive.Description>) { return ( <McDrawerPrimitive.Description data-slot="drawer-description" className={cn('text-sm text-muted-foreground leading-relaxed', className)} {...props} /> );}function McDrawerNav({ className, ...props }: React.ComponentProps<'div'>) { return ( <div data-slot="drawer-nav" className={cn('flex h-drawer-nav items-center gap-2 px-6 py-2.5', className)} {...props} > <McDrawerClose className="flex h-8 w-8 items-center justify-center rounded-md text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50" aria-label="Go back" > <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true"> <path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> </McDrawerClose> </div> );}interface McDrawerNavItemProps extends React.ComponentProps<'button'> { icon?: React.ReactNode; hasArrow?: boolean; rightElement?: React.ReactNode;}function McDrawerNavItem({ icon, hasArrow = false, rightElement, children, className, ...props}: McDrawerNavItemProps) { return ( <button data-slot="drawer-nav-item" className={cn( 'flex h-drawer-item w-full items-center justify-between rounded-xl', 'px-6 py-5', 'text-sm text-foreground', 'transition-colors hover:bg-accent hover:text-accent-foreground', 'focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50', className )} {...props} > <span className="flex items-center gap-3"> {icon && ( <span className="shrink-0 text-foreground" aria-hidden="true"> {icon} </span> )} {children} </span> {rightElement ?? (hasArrow && ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"> <path d="M6 4L10 8L6 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> ))} </button> );}export { McDrawer, McDrawerPortal, McDrawerOverlay, McDrawerTrigger, McDrawerClose, McDrawerContent, McDrawerHeader, McDrawerFooter, McDrawerTitle, McDrawerDescription, McDrawerNav, McDrawerNavItem,};Adjust import paths
Ensure your @/lib/utils or equivalent classname helper is correctly imported.
Usage
import {
McDrawer,
McDrawerContent,
McDrawerDescription,
McDrawerHeader,
McDrawerTitle,
McDrawerTrigger,
} from '@/components/ui/mc-drawer';
export default function Example() {
return (
<McDrawer direction="right">
<McDrawerTrigger>Open drawer</McDrawerTrigger>
<McDrawerContent>
<McDrawerHeader>
<McDrawerTitle>Settings</McDrawerTitle>
<McDrawerDescription>Update your workspace preferences.</McDrawerDescription>
</McDrawerHeader>
</McDrawerContent>
</McDrawer>
);
}Examples
Navigation Drawer
import { Moon } from 'lucide-react';
import {
McDrawer,
McDrawerContent,
McDrawerNav,
McDrawerNavItem,
McDrawerTitle,
McDrawerTrigger,
} from '@/components/ui/mc-drawer';
export function NavigationDrawer() {
return (
<McDrawer direction="right">
<McDrawerTrigger>Open navigation</McDrawerTrigger>
<McDrawerContent>
<McDrawerTitle className="sr-only">Navigation</McDrawerTitle>
<McDrawerNav />
<div className="flex flex-1 flex-col overflow-y-auto pt-9">
<McDrawerNavItem icon={<Moon size={16} />} hasArrow>
Account
</McDrawerNavItem>
<McDrawerNavItem icon={<Moon size={16} />} hasArrow>
Settings
</McDrawerNavItem>
</div>
</McDrawerContent>
</McDrawer>
);
}Bottom Sheet
import {
McDrawer,
McDrawerClose,
McDrawerContent,
McDrawerDescription,
McDrawerHeader,
McDrawerTitle,
McDrawerTrigger,
} from '@/components/ui/mc-drawer';
export function ProfileDrawer() {
return (
<McDrawer direction="bottom">
<McDrawerTrigger>Edit profile</McDrawerTrigger>
<McDrawerContent>
<div className="flex flex-col gap-6 px-4 py-6">
<McDrawerHeader className="gap-2 p-0">
<McDrawerTitle>Edit profile</McDrawerTitle>
<McDrawerDescription>Save your changes before closing the drawer.</McDrawerDescription>
</McDrawerHeader>
<input
type="email"
defaultValue="m@example.com"
className="w-full rounded-lg border border-border bg-input px-4 py-3 text-sm"
/>
<McDrawerClose className="w-full rounded-lg bg-primary py-4 text-sm font-medium text-primary-foreground">
Save changes
</McDrawerClose>
</div>
</McDrawerContent>
</McDrawer>
);
}API Reference
Root Components
| Component | Description |
|---|---|
McDrawer | Root wrapper that manages drawer state and direction |
McDrawerTrigger | Element that opens the drawer |
McDrawerContent | Drawer panel rendered in a portal with overlay support |
McDrawerPortal | Portal wrapper from the underlying drawer primitive |
McDrawerOverlay | Fixed overlay displayed behind the drawer content |
McDrawerClose | Element that closes the drawer |
Layout Components
| Component | Description |
|---|---|
McDrawerHeader | Header region for titles and descriptions |
McDrawerFooter | Footer region for actions |
McDrawerTitle | Accessible drawer title |
McDrawerDescription | Accessible supporting description |
McDrawerNav | Top navigation row with a built-in close control |
McDrawerNavItem | Full-width drawer navigation item |
Common Props
McDrawer
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "top" | "right" | "bottom" | "left" | "bottom" | Side where the drawer opens |
open | boolean | undefined | Controlled open state |
onOpenChange | (open: boolean) => void | undefined | Callback fired when open state changes |
McDrawerContent
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | undefined | Additional classes for the panel |
children | ReactNode | undefined | Content rendered inside the drawer |
McDrawerNavItem
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | undefined | Optional leading icon |
hasArrow | boolean | false | Shows a trailing chevron |
rightElement | ReactNode | undefined | Custom trailing element, such as a switch |
className | string | undefined | Additional classes for the item |
All other relevant props from vaul are supported on their matching wrappers.