Sheet
A side panel that slides in over the page for details, forms, and secondary flows.
npx shadcn@latest add @velaui/sheetpnpm dlx velaui add @velaui/sheetyarn dlx shadcn@latest add @velaui/sheetbunx --bun shadcn@latest add @velaui/sheetveni add sheetInstalls the copy of vela-ui that veni-builder serves to Venizia apps; veni-builder serves the Radix UI copy only, so this tab exists for that base alone.
npx shadcn@latest add @velaui/sheetpnpm dlx velaui add @velaui/sheetyarn dlx shadcn@latest add @velaui/sheetbunx --bun shadcn@latest add @velaui/sheetExamples
- Default
- Badge and tabs
- Additional action
- Side left
- Form
- Side bottom
Usage
import {
Sheet,
SheetBody,
SheetClose,
SheetCloseButton,
SheetContent,
SheetDescription,
SheetFooter,
SheetFooterActions,
SheetHeader,
SheetHeaderContent,
SheetTitle,
SheetTitleGroup,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
export default function Example() {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="primary">Open panel</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetHeaderContent>
<SheetTitleGroup>
<SheetTitle>Master Heading</SheetTitle>
<SheetDescription>
A side panel slides in over the page to show details, forms or
secondary flows without losing the current context.
</SheetDescription>
</SheetTitleGroup>
<SheetCloseButton />
</SheetHeaderContent>
</SheetHeader>
<SheetBody>
<p className="text-sm text-text-muted">
Everything the panel has to say goes here. The body scrolls on its
own, so the heading and the actions stay put.
</p>
</SheetBody>
<SheetFooter>
<SheetFooterActions>
<SheetClose asChild>
<Button variant="secondary">Cancel</Button>
</SheetClose>
<SheetClose asChild>
<Button variant="primary">Save changes</Button>
</SheetClose>
</SheetFooterActions>
</SheetFooter>
</SheetContent>
</Sheet>
);
}import {
Sheet,
SheetBody,
SheetClose,
SheetCloseButton,
SheetContent,
SheetDescription,
SheetFooter,
SheetFooterActions,
SheetHeader,
SheetHeaderContent,
SheetTitle,
SheetTitleGroup,
SheetTrigger,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
export default function Example() {
return (
<Sheet>
<SheetTrigger render={<Button variant="primary" />}>
Open panel
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetHeaderContent>
<SheetTitleGroup>
<SheetTitle>Master Heading</SheetTitle>
<SheetDescription>
A side panel slides in over the page to show details, forms or
secondary flows without losing the current context.
</SheetDescription>
</SheetTitleGroup>
<SheetCloseButton />
</SheetHeaderContent>
</SheetHeader>
<SheetBody>
<p className="text-sm text-text-muted">
Everything the panel has to say goes here. The body scrolls on its
own, so the heading and the actions stay put.
</p>
</SheetBody>
<SheetFooter>
<SheetFooterActions>
<SheetClose render={<Button variant="secondary" />}>
Cancel
</SheetClose>
<SheetClose render={<Button variant="primary" />}>
Save changes
</SheetClose>
</SheetFooterActions>
</SheetFooter>
</SheetContent>
</Sheet>
);
}Props
Sheet
Takes every prop of SheetProps.
SheetBody
Takes every prop of SheetBodyProps.
SheetClose
Takes every prop of SheetCloseProps.
SheetCloseButton
Takes every prop of SheetCloseButtonProps.
SheetContent
| Prop | Type | Default |
|---|---|---|
side | "top" | "bottom" | "left" | "right" | "right" |
Takes these and every prop of SheetContentProps.
SheetDescription
Takes every prop of SheetDescriptionProps.
SheetFooter
Takes every prop of SheetFooterProps.
SheetFooterActions
Takes every prop of SheetFooterActionsProps.
SheetHeader
Takes every prop of SheetHeaderProps.
SheetHeaderContent
Takes every prop of SheetHeaderContentProps.
SheetOverlay
Takes every prop of SheetOverlayProps.
SheetPortal
Takes every prop of SheetPortalProps.
SheetTitle
Takes every prop of SheetTitleProps.
SheetTitleGroup
Takes every prop of SheetTitleGroupProps.
SheetTrigger
Takes every prop of SheetTriggerProps.
Dependencies
- npm
radix-ui@1.6.0- Installs with it
- 1 icon
- npm
@base-ui/react@1.6.0- Installs with it
- 1 icon
Source
components/ui/sheet.tsx
import * as React from "react";
import { Dialog as SheetPrimitive } from "radix-ui";
import { Close02 } from "@/components/icons/close-02";
import { cn } from "@/lib/utils";
export type SheetProps = React.ComponentProps<typeof SheetPrimitive.Root>;
function Sheet({ ...props }: SheetProps) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
export type SheetTriggerProps = React.ComponentProps<typeof SheetPrimitive.Trigger>;
function SheetTrigger({
...props
}: SheetTriggerProps) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
export type SheetCloseProps = React.ComponentProps<typeof SheetPrimitive.Close>;
function SheetClose({
...props
}: SheetCloseProps) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
export type SheetPortalProps = React.ComponentProps<typeof SheetPrimitive.Portal>;
function SheetPortal({
...props
}: SheetPortalProps) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
export type SheetOverlayProps = React.ComponentProps<typeof SheetPrimitive.Overlay>;
function SheetOverlay({
className,
...props
}: SheetOverlayProps) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/10 duration-100 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}
/>
);
}
export type SheetContentProps = React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
};
/**
* The panel floats: it carries a full border, a 24px radius on every corner and
* a bottom shadow, so it sits inset from the viewport edge rather than pinned
* flush to it. Horizontal padding lives here; every band supplies its own
* vertical padding.
*/
function SheetContent({
className,
children,
side = "right",
...props
}: SheetContentProps) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col overflow-hidden rounded-3xl border border-border-default bg-bg-main px-6 text-sm text-text-strong shadow-xs transition duration-200 ease-in-out outline-none",
"data-[side=right]:inset-y-4 data-[side=right]:right-4 data-[side=right]:w-3/4 data-[side=right]:sm:max-w-[605px]",
"data-[side=left]:inset-y-4 data-[side=left]:left-4 data-[side=left]:w-3/4 data-[side=left]:sm:max-w-[605px]",
"data-[side=top]:inset-x-4 data-[side=top]:top-4 data-[side=top]:h-auto",
"data-[side=bottom]:inset-x-4 data-[side=bottom]:bottom-4 data-[side=bottom]:h-auto",
"data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10",
"data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
className,
)}
{...props}
>
{children}
</SheetPrimitive.Content>
</SheetPortal>
);
}
export type SheetHeaderProps = React.ComponentProps<"div">;
/** The whole header band — the title block, and whatever follows it (tabs). */
function SheetHeader({ className, ...props }: SheetHeaderProps) {
return (
<div
data-slot="sheet-header"
className={cn("flex w-full flex-col gap-8 pt-6", className)}
{...props}
/>
);
}
export type SheetHeaderContentProps = React.ComponentProps<"div">;
/**
* The title block plus anything attached to it (a badge), and the anchor for
* the close button. Sits above the header's own gap, so tabs placed as its
* sibling clear it by 32px.
*/
function SheetHeaderContent({
className,
...props
}: SheetHeaderContentProps) {
return (
<div
data-slot="sheet-header-content"
className={cn("relative flex w-full flex-col gap-2", className)}
{...props}
/>
);
}
export type SheetTitleGroupProps = React.ComponentProps<"div">;
/** Title and description, kept clear of the close button by the right padding. */
function SheetTitleGroup({ className, ...props }: SheetTitleGroupProps) {
return (
<div
data-slot="sheet-title-group"
className={cn("flex w-full flex-col gap-1 pr-10", className)}
{...props}
/>
);
}
export type SheetCloseButtonProps = React.ComponentProps<typeof SheetPrimitive.Close>;
/** The bare 16px dismiss glyph pinned to the top-right of the title block. */
function SheetCloseButton({
className,
...props
}: SheetCloseButtonProps) {
return (
<SheetPrimitive.Close
data-slot="sheet-close"
className={cn(
"absolute top-0 right-0 inline-flex size-4 items-center justify-center rounded-xs text-icon-strong transition-colors focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow focus-visible:outline-none [&_svg]:size-4",
className,
)}
{...props}
>
<Close02 />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
);
}
export type SheetBodyProps = React.ComponentProps<"div">;
/** The scrolling middle band. */
function SheetBody({ className, ...props }: SheetBodyProps) {
return (
<div
data-slot="sheet-body"
className={cn(
"flex w-full min-h-0 flex-1 flex-col gap-4 overflow-y-auto py-8",
className,
)}
{...props}
/>
);
}
export type SheetFooterProps = React.ComponentProps<"div">;
function SheetFooter({ className, ...props }: SheetFooterProps) {
return (
<div
data-slot="sheet-footer"
className={cn(
"flex w-full items-center justify-end gap-4 py-6",
className,
)}
{...props}
/>
);
}
export type SheetFooterActionsProps = React.ComponentProps<"div">;
/**
* The right-aligned action group. Taking the free space is what pushes an
* additional action placed before it to the far left of the footer.
*/
function SheetFooterActions({
className,
...props
}: SheetFooterActionsProps) {
return (
<div
data-slot="sheet-footer-actions"
className={cn(
"flex min-w-0 flex-1 items-center justify-end gap-2",
className,
)}
{...props}
/>
);
}
export type SheetTitleProps = React.ComponentProps<typeof SheetPrimitive.Title>;
function SheetTitle({
className,
...props
}: SheetTitleProps) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
"text-3xl leading-[1.2] font-semibold text-text-strong",
className,
)}
{...props}
/>
);
}
export type SheetDescriptionProps = React.ComponentProps<typeof SheetPrimitive.Description>;
function SheetDescription({
className,
...props
}: SheetDescriptionProps) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm leading-[1.4] text-text-muted", className)}
{...props}
/>
);
}
export {
Sheet,
SheetBody,
SheetClose,
SheetCloseButton,
SheetContent,
SheetDescription,
SheetFooter,
SheetFooterActions,
SheetHeader,
SheetHeaderContent,
SheetOverlay,
SheetPortal,
SheetTitle,
SheetTitleGroup,
SheetTrigger,
};components/ui/sheet.tsx
import * as React from "react";
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
import { Close02 } from "@/components/icons/close-02";
import { cn } from "@/lib/utils";
export type SheetProps = SheetPrimitive.Root.Props;
function Sheet({ ...props }: SheetProps) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
export type SheetTriggerProps = SheetPrimitive.Trigger.Props;
function SheetTrigger({ ...props }: SheetTriggerProps) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
export type SheetCloseProps = SheetPrimitive.Close.Props;
function SheetClose({ ...props }: SheetCloseProps) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
export type SheetPortalProps = SheetPrimitive.Portal.Props;
function SheetPortal({ ...props }: SheetPortalProps) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
export type SheetOverlayProps = SheetPrimitive.Backdrop.Props;
function SheetOverlay({
className,
...props
}: SheetOverlayProps) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-black/10 duration-100 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}
/>
);
}
export type SheetContentProps = SheetPrimitive.Popup.Props & {
side?: "top" | "right" | "bottom" | "left";
};
/**
* The panel floats: it carries a full border, a 24px radius on every corner and
* a bottom shadow, so it sits inset from the viewport edge rather than pinned
* flush to it. Horizontal padding lives here; every band supplies its own
* vertical padding.
*/
function SheetContent({
className,
children,
side = "right",
...props
}: SheetContentProps) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
"fixed z-50 flex flex-col overflow-hidden rounded-3xl border border-border-default bg-bg-main px-6 text-sm text-text-strong shadow-xs transition duration-200 ease-in-out outline-none",
"data-[side=right]:inset-y-4 data-[side=right]:right-4 data-[side=right]:w-3/4 data-[side=right]:sm:max-w-[605px]",
"data-[side=left]:inset-y-4 data-[side=left]:left-4 data-[side=left]:w-3/4 data-[side=left]:sm:max-w-[605px]",
"data-[side=top]:inset-x-4 data-[side=top]:top-4 data-[side=top]:h-auto",
"data-[side=bottom]:inset-x-4 data-[side=bottom]:bottom-4 data-[side=bottom]:h-auto",
"data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10",
"data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
className,
)}
{...props}
>
{children}
</SheetPrimitive.Popup>
</SheetPortal>
);
}
export type SheetHeaderProps = React.ComponentProps<"div">;
/** The whole header band — the title block, and whatever follows it (tabs). */
function SheetHeader({ className, ...props }: SheetHeaderProps) {
return (
<div
data-slot="sheet-header"
className={cn("flex w-full flex-col gap-8 pt-6", className)}
{...props}
/>
);
}
export type SheetHeaderContentProps = React.ComponentProps<"div">;
/**
* The title block plus anything attached to it (a badge), and the anchor for
* the close button. Sits above the header's own gap, so tabs placed as its
* sibling clear it by 32px.
*/
function SheetHeaderContent({
className,
...props
}: SheetHeaderContentProps) {
return (
<div
data-slot="sheet-header-content"
className={cn("relative flex w-full flex-col gap-2", className)}
{...props}
/>
);
}
export type SheetTitleGroupProps = React.ComponentProps<"div">;
/** Title and description, kept clear of the close button by the right padding. */
function SheetTitleGroup({ className, ...props }: SheetTitleGroupProps) {
return (
<div
data-slot="sheet-title-group"
className={cn("flex w-full flex-col gap-1 pr-10", className)}
{...props}
/>
);
}
export type SheetCloseButtonProps = SheetPrimitive.Close.Props;
/** The bare 16px dismiss glyph pinned to the top-right of the title block. */
function SheetCloseButton({
className,
...props
}: SheetCloseButtonProps) {
return (
<SheetPrimitive.Close
data-slot="sheet-close"
className={cn(
"absolute top-0 right-0 inline-flex size-4 items-center justify-center rounded-xs text-icon-strong transition-colors focus-visible:ring-3 focus-visible:ring-ring-neutral-shadow focus-visible:outline-none [&_svg]:size-4",
className,
)}
{...props}
>
<Close02 />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
);
}
export type SheetBodyProps = React.ComponentProps<"div">;
/** The scrolling middle band. */
function SheetBody({ className, ...props }: SheetBodyProps) {
return (
<div
data-slot="sheet-body"
className={cn(
"flex w-full min-h-0 flex-1 flex-col gap-4 overflow-y-auto py-8",
className,
)}
{...props}
/>
);
}
export type SheetFooterProps = React.ComponentProps<"div">;
function SheetFooter({ className, ...props }: SheetFooterProps) {
return (
<div
data-slot="sheet-footer"
className={cn(
"flex w-full items-center justify-end gap-4 py-6",
className,
)}
{...props}
/>
);
}
export type SheetFooterActionsProps = React.ComponentProps<"div">;
/**
* The right-aligned action group. Taking the free space is what pushes an
* additional action placed before it to the far left of the footer.
*/
function SheetFooterActions({
className,
...props
}: SheetFooterActionsProps) {
return (
<div
data-slot="sheet-footer-actions"
className={cn(
"flex min-w-0 flex-1 items-center justify-end gap-2",
className,
)}
{...props}
/>
);
}
export type SheetTitleProps = SheetPrimitive.Title.Props;
function SheetTitle({ className, ...props }: SheetTitleProps) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
"text-3xl leading-[1.2] font-semibold text-text-strong",
className,
)}
{...props}
/>
);
}
export type SheetDescriptionProps = SheetPrimitive.Description.Props;
function SheetDescription({
className,
...props
}: SheetDescriptionProps) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-sm leading-[1.4] text-text-muted", className)}
{...props}
/>
);
}
export {
Sheet,
SheetBody,
SheetClose,
SheetCloseButton,
SheetContent,
SheetDescription,
SheetFooter,
SheetFooterActions,
SheetHeader,
SheetHeaderContent,
SheetOverlay,
SheetPortal,
SheetTitle,
SheetTitleGroup,
SheetTrigger,
};