Drawer
A draggable edge panel for supporting tasks, filters, and compact workflows.
Preview
Example code
Playground
modalMakes content outside the drawer unavailable while open.
positionSets the edge used for placement and drag gestures.
Usage
tsx
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "earthling-ui/drawer";
export function Example() {
return (
<>
<Drawer position="bottom">
<DrawerTrigger>Review filters</DrawerTrigger>
<DrawerContent>
<div className="flex min-w-0 flex-1 flex-col">
<DrawerHeader>
<DrawerTitle>Filter activity</DrawerTitle>
<DrawerDescription>
Narrow the activity feed by status.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose>Close</DrawerClose>
</DrawerFooter>
</div>
</DrawerContent>
</Drawer>
</>
);
}
- Drawer position is shared with DrawerContent, so one position prop keeps gestures and layout aligned.
- Include DrawerTitle and DrawerDescription even when their text is visually hidden.
- Use a dialog for short confirmations; drawers work best for progressive or mobile-friendly tasks.
Installation
Start with the project setup for React and Tailwind CSS 4. Both paths use the same component and theme.
Import package
Own the source
bash
npm install earthling-uiUse the component import shown above. Package updates keep the implementation current.
API reference
Declarations from the current library build, including inherited primitive props. Playground controls above show selected options; they are not the complete API.
tsx
// Generated by dts-bundle-generator v9.5.1
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { VariantProps } from 'class-variance-authority';
import { ComponentProps, ComponentPropsWithoutRef, ComponentRef, ForwardRefExoticComponent, RefAttributes } from 'react';
import { Drawer as DrawerPrimitive } from 'vaul';
export type DrawerProps = ComponentProps<typeof DrawerPrimitive.Root> & {
position?: "left" | "right" | "top" | "bottom";
};
export declare const Drawer: {
({ position, direction, children, ...props }: DrawerProps): import("react").JSX.Element;
displayName: string;
};
export type DrawerTriggerPrimitive = typeof DrawerPrimitive.Trigger;
export type DrawerTriggerProps = ComponentProps<DrawerTriggerPrimitive>;
export declare const DrawerTrigger: React.FC<DrawerTriggerProps>;
export type DrawerPortalProps = ComponentProps<typeof DrawerPrimitive.Portal>;
export declare const DrawerPortal: React.FC<DrawerPortalProps>;
export type DrawerCloseProps = ComponentProps<typeof DrawerPrimitive.Close>;
export declare const DrawerClose: React.FC<DrawerCloseProps>;
export type DrawerOverlayPrimitive = typeof DrawerPrimitive.Overlay;
export type DrawerOverlayProps = ComponentPropsWithoutRef<DrawerOverlayPrimitive>;
export declare const DrawerOverlay: ForwardRefExoticComponent<DrawerOverlayProps & RefAttributes<ComponentRef<DrawerOverlayPrimitive>>>;
declare const drawerContentVariants: (props?: ({
position?: "left" | "right" | "top" | "bottom" | null | undefined;
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
export interface DrawerContentProps extends ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>, VariantProps<typeof drawerContentVariants> {
}
export declare const DrawerContent: ForwardRefExoticComponent<DrawerContentProps & RefAttributes<HTMLDivElement>>;
export declare const DrawerHeader: {
({ className, ...props }: ComponentProps<"div">): import("react").JSX.Element;
displayName: string;
};
export declare const DrawerFooter: {
({ className, ...props }: ComponentProps<"div">): import("react").JSX.Element;
displayName: string;
};
export type DrawerTitlePrimitive = typeof DrawerPrimitive.Title;
export type DrawerTitleProps = ComponentPropsWithoutRef<DrawerTitlePrimitive>;
export declare const DrawerTitle: ForwardRefExoticComponent<DrawerTitleProps & RefAttributes<ComponentRef<DrawerTitlePrimitive>>>;
export type DrawerDescriptionPrimitive = typeof DrawerPrimitive.Description;
export type DrawerDescriptionProps = ComponentPropsWithoutRef<DrawerDescriptionPrimitive>;
export declare const DrawerDescription: ForwardRefExoticComponent<DrawerDescriptionProps & RefAttributes<ComponentRef<DrawerDescriptionPrimitive>>>;
export {};Runtime dependencies: class-variance-authority, cnfast, vaul.
Source
Use the eject command to copy this implementation with its required helpers and project-specific imports.
View Drawer implementation
tsx
"use client";
import {
createContext,
forwardRef,
useContext,
type ComponentProps,
type ComponentPropsWithoutRef,
type ComponentRef,
type ForwardRefExoticComponent,
type RefAttributes,
} from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/utils/cn";
import { cva, type VariantProps } from "class-variance-authority";
const DrawerContext = createContext<{
position: "left" | "right" | "top" | "bottom";
}>({
position: "bottom",
});
export type DrawerProps = ComponentProps<typeof DrawerPrimitive.Root> & {
position?: "left" | "right" | "top" | "bottom";
};
const Drawer = ({
position = "bottom",
direction = position,
children,
...props
}: DrawerProps) => (
<DrawerPrimitive.Root direction={direction} {...props}>
<DrawerContext.Provider value={{ position: direction }}>
{children}
</DrawerContext.Provider>
</DrawerPrimitive.Root>
);
Drawer.displayName = "Drawer";
type DrawerTriggerPrimitive = typeof DrawerPrimitive.Trigger;
export type DrawerTriggerProps = ComponentProps<DrawerTriggerPrimitive>;
const DrawerTrigger = DrawerPrimitive.Trigger as React.FC<DrawerTriggerProps>;
export type DrawerPortalProps = ComponentProps<typeof DrawerPrimitive.Portal>;
const DrawerPortal = DrawerPrimitive.Portal as React.FC<DrawerPortalProps>;
export type DrawerCloseProps = ComponentProps<typeof DrawerPrimitive.Close>;
const DrawerClose = DrawerPrimitive.Close as React.FC<DrawerCloseProps>;
type DrawerOverlayPrimitive = typeof DrawerPrimitive.Overlay;
export type DrawerOverlayProps =
ComponentPropsWithoutRef<DrawerOverlayPrimitive>;
const DrawerOverlay: ForwardRefExoticComponent<
DrawerOverlayProps & RefAttributes<ComponentRef<DrawerOverlayPrimitive>>
> = forwardRef<ComponentRef<DrawerOverlayPrimitive>, DrawerOverlayProps>(
({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-muted/60 duration-150 ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 motion-reduce:animate-none",
className,
)}
{...props}
/>
),
);
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
const drawerContentVariants = cva(
"fixed z-50 flex h-auto overflow-auto bg-background outline-none motion-reduce:transition-none",
{
variants: {
position: {
bottom:
"inset-x-0 bottom-0 max-h-[calc(100dvh-1rem)] flex-col rounded-t-xl border-t",
top: "inset-x-0 top-0 max-h-[calc(100dvh-1rem)] flex-col-reverse rounded-b-xl border-b",
left: "inset-y-0 left-0 max-w-[calc(100vw-1rem)] flex-row-reverse rounded-r-xl border-r",
right:
"inset-y-0 right-0 max-w-[calc(100vw-1rem)] flex-row rounded-l-xl border-l",
},
},
defaultVariants: {
position: "bottom",
},
},
);
const drawerHandleVariants = cva("rounded-full bg-muted", {
variants: {
position: {
bottom: "mx-auto mt-4 h-1.5 w-24",
top: "mx-auto mb-4 h-1.5 w-24",
left: "my-auto me-4 h-24 w-1.5",
right: "my-auto ms-4 h-24 w-1.5",
},
},
defaultVariants: {
position: "bottom",
},
});
export interface DrawerContentProps
extends
ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>,
VariantProps<typeof drawerContentVariants> {}
const DrawerContent = forwardRef<
ComponentRef<typeof DrawerPrimitive.Content>,
DrawerContentProps
>(({ className, children, position, ...props }, ref) => {
const context = useContext(DrawerContext);
return (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
drawerContentVariants({
position: position || context.position,
}),
className,
)}
{...props}
>
<div
className={cn(
drawerHandleVariants({
position: position || context.position,
}),
)}
/>
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
});
DrawerContent.displayName = "DrawerContent";
const DrawerHeader = ({ className, ...props }: ComponentProps<"div">) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
);
DrawerHeader.displayName = "DrawerHeader";
const DrawerFooter = ({ className, ...props }: ComponentProps<"div">) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
DrawerFooter.displayName = "DrawerFooter";
type DrawerTitlePrimitive = typeof DrawerPrimitive.Title;
export type DrawerTitleProps = ComponentPropsWithoutRef<DrawerTitlePrimitive>;
const DrawerTitle: ForwardRefExoticComponent<
DrawerTitleProps & RefAttributes<ComponentRef<DrawerTitlePrimitive>>
> = forwardRef<ComponentRef<DrawerTitlePrimitive>, DrawerTitleProps>(
({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight text-balance",
className,
)}
{...props}
/>
),
);
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
type DrawerDescriptionPrimitive = typeof DrawerPrimitive.Description;
export type DrawerDescriptionProps =
ComponentPropsWithoutRef<DrawerDescriptionPrimitive>;
const DrawerDescription: ForwardRefExoticComponent<
DrawerDescriptionProps &
RefAttributes<ComponentRef<DrawerDescriptionPrimitive>>
> = forwardRef<
ComponentRef<DrawerDescriptionPrimitive>,
DrawerDescriptionProps
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground text-pretty", className)}
{...props}
/>
));
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};