Overlays
Sheet.
A dialog that slides in from an edge of the screen, for settings panels and mobile navigation.
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
export default function SheetDemo() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="secondary">Server settings</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader className="border-b">
<SheetTitle>Server settings</SheetTitle>
<SheetDescription>Where your hub is hosted and how it reaches the relay.</SheetDescription>
</SheetHeader>
<div className="grid gap-4 px-5">
<div className="grid gap-2">
<Label htmlFor="sheet-computer">Computer ID</Label>
<Input id="sheet-computer" defaultValue="42" inputMode="numeric" />
</div>
<div className="grid gap-2">
<Label htmlFor="sheet-relay">Relay channel</Label>
<Input id="sheet-relay" defaultValue="lantern-main" />
</div>
</div>
<SheetFooter className="border-t">
<SheetClose asChild>
<Button>Save settings</Button>
</SheetClose>
<SheetClose asChild>
<Button variant="secondary">Cancel</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
}Installation#
With the CLI
$npx lanterncn add sheetThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/sheet. See Installation if your project is not set up yet.
Manually
Install the dependencies
npm install radix-ui lucide-reactCopy the source into your project
components/ui/sheet.tsx"use client"; import * as React from "react"; import { Dialog as SheetPrimitive } from "radix-ui"; import { XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { return <SheetPrimitive.Root data-slot="sheet" {...props} />; } function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPrimitive.Trigger>) { return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />; } function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimitive.Close>) { return <SheetPrimitive.Close data-slot="sheet-close" {...props} />; } function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) { return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />; } function SheetOverlay({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Overlay>) { return ( <SheetPrimitive.Overlay data-slot="sheet-overlay" className={cn( "fixed inset-0 z-50 bg-[#080b0a]/75 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className, )} {...props} /> ); } function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & { side?: "top" | "right" | "bottom" | "left"; showCloseButton?: boolean; }) { return ( <SheetPortal> <SheetOverlay /> <SheetPrimitive.Content data-slot="sheet-content" data-side={side} className={cn( "fixed z-50 flex flex-col gap-4 overflow-y-auto bg-popover text-popover-foreground outline-none transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:animate-in data-[state=open]:duration-300", side === "right" && "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", side === "left" && "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", side === "top" && "inset-x-0 top-0 h-auto max-h-[85dvh] border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", side === "bottom" && "inset-x-0 bottom-0 h-auto max-h-[85dvh] border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", className, )} {...props} > {children} {showCloseButton && ( <SheetPrimitive.Close data-slot="sheet-close" className="absolute top-4 right-4 inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-popover disabled:pointer-events-none [&_svg]:size-4" > <XIcon /> <span className="sr-only">Close</span> </SheetPrimitive.Close> )} </SheetPrimitive.Content> </SheetPortal> ); } function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="sheet-header" className={cn("flex flex-col gap-1.5 p-5 pr-12", className)} {...props} />; } function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="sheet-footer" className={cn("mt-auto flex flex-col gap-2 p-5", className)} {...props} />; } function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) { return ( <SheetPrimitive.Title data-slot="sheet-title" className={cn("font-display text-lg leading-tight font-medium tracking-tight", className)} {...props} /> ); } function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) { return ( <SheetPrimitive.Description data-slot="sheet-description" className={cn("text-sm text-muted-foreground", className)} {...props} /> ); } export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";<Sheet>
<SheetTrigger asChild>
<Button>Open</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Server settings</SheetTitle>
<SheetDescription>Where your hub is hosted.</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>Set side to top, right, bottom or left. Left and right sheets take three quarters of the screen on phones and cap at 24rem on larger screens.
Examples#
Sides
import { Button } from "@/components/ui/button";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
const sides = ["top", "right", "bottom", "left"] as const;
export default function SheetSides() {
return (
<div className="grid grid-cols-2 gap-3 sm:flex sm:flex-wrap">
{sides.map((side) => (
<Sheet key={side}>
<SheetTrigger asChild>
<Button variant="outline" className="capitalize">
{side}
</Button>
</SheetTrigger>
<SheetContent side={side}>
<SheetHeader>
<SheetTitle className="capitalize">{side} sheet</SheetTitle>
<SheetDescription>Slides in from the {side} edge. Press Escape or click outside to close.</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
))}
</div>
);
}