Sheet
Side drawer that slides in from any edge (top / bottom / left / right). One of the most-used surfaces in the Yuno Dashboard — long create forms (payment link, rule, blocklist), filter panels, detail editors, quick create/edit forms, properties inspectors. Built on Radix Dialog with Yuno-token chrome (Header + Body + Footer stacked in a flex-col, bg-background, shadow-lg, dimmer bg-foreground/24).
Anatomy
Five parts. Trigger and Content, plus three composable regions inside the Content: Header (bordered top row with the title + optional back), Body (the scrollable main area), Footer (bordered bottom row for the CTAs). Content stacks all three in a flex-col so the body absorbs the remaining height.
- 1SheetTrigger
The button (or any element via asChild) that opens the sheet. Radix wires up the state — no controlled prop needed.
- 2SheetContent (side)
The panel. side='right' (default) / 'left' / 'top' / 'bottom'. Right and Left ship with w-full min-w-80 (320) max-w-3xl (768) — defaults to the maximum 768px on desktop and gracefully shrinks (capped by min-w-80) on narrow viewports. Top and Bottom fill the width; bottom has rounded-t-3xl per Yuno mobile convention.
- 3SheetHeader
Bordered top row: p-6 border-b flex-col gap-1.5 text-left with pr-14 reserved for the close X. Holds SheetTitle + optional SheetDescription. Matches the Figma spec exactly.
- 4SheetBody
The scrollable middle region: flex-1 overflow-auto p-6. Fills the space between Header and Footer. When content overflows, only the body scrolls — Header and Footer stay pinned.
- 5SheetFooter
Bordered bottom row: p-6 border-t flex flex-col-reverse gap-2 sm:flex-row sm:justify-end. Anchor the primary CTA on the right (desktop) or full-width on top of the stack (mobile).
- 6Close X
Absolutely positioned at top-6 right-6, 16×16 (size-4), rounded-xs, opacity-70 with hover opacity-100 and a focus-visible ring. Rendered automatically by SheetContent — no need to add it manually.
Variants
Four sides. Right is the canonical for detail/edit flows (matches reading direction). Left is common for navigation drawers. Top/Bottom fill the width — bottom sheets get the Yuno rounded-t-3xl mobile treatment.
<Sheet>
<SheetTrigger asChild>
<Button>Open</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Sheet title</SheetTitle>
</SheetHeader>
<SheetBody>{/* content */}</SheetBody>
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Cancel</Button>
</SheetClose>
<Button>Save</Button>
</SheetFooter>
</SheetContent>
</Sheet><SheetContent side="left">
<SheetHeader>
<SheetTitle>Sheet title</SheetTitle>
</SheetHeader>
<SheetBody>{/* content */}</SheetBody>
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Cancel</Button>
</SheetClose>
<Button>Save</Button>
</SheetFooter>
</SheetContent><SheetContent side="top">
<SheetHeader>
<SheetTitle>Sheet title</SheetTitle>
</SheetHeader>
<SheetBody>{/* content */}</SheetBody>
</SheetContent><SheetContent side="bottom">
<SheetHeader>
<SheetTitle>Sheet title</SheetTitle>
</SheetHeader>
<SheetBody>{/* content */}</SheetBody>
<SheetFooter>
<Button>Confirm</Button>
</SheetFooter>
</SheetContent>Width bounds (right and left)
Right and Left sheets ship with a 320px min-width and a 768px max-width. Default is w-full so on desktop the panel expands to the max (768px), giving prototypes the most room without any override needed. Below the max, w-full tracks the viewport down to the 320px floor. Override via className when a screen genuinely needs a slimmer panel (e.g. sm:max-w-md for a compact settings sheet) — but 768 is intentionally the ceiling.
States
Overlay: fade-in 450ms on open, fade-out 200ms on close. Content: slides from the anchored edge — right sheet enters from off-right, left from off-left, top from above, bottom from below. Entry 300ms with ease-out (0.22, 1, 0.36, 1); exit 200ms with ease-in (0.55, 0, 1, 0.45) so the panel accelerates out. SheetBody children cascade in with a 40ms stagger starting at 220ms — form fields, copy blocks and any block-level content land one after another after the sheet finishes sliding. Close X: opacity-70 idle, opacity-100 on hover, focus-visible ring on keyboard focus. Respects prefers-reduced-motion — cascade turns off automatically.
Motion
Sheets use side-specific slide-in/slide-out — never a plain fade — so the direction of entry matches the anchored edge. SheetBody children cascade after the panel lands so the eye follows content in the order it will be edited.
| Overlay | fade-in 450ms ease-out → fade-out 200ms ease-in (bg-foreground/24, no blur). |
|---|---|
| Content — right side | slide from translateX(100%) → 0 on open, reverse on close. Entry 300ms ease-out, exit 200ms ease-in. |
| Content — left side | slide from translateX(-100%) → 0 on open, reverse on close. Same timing as right. |
| Content — top side | slide from translateY(-100%) → 0 on open, reverse on close. Same timing. |
| Content — bottom side | slide from translateY(100%) → 0 on open, reverse on close. Same timing. |
| SheetBody cascade | Direct children fade-up (cascade-in) with 40ms stagger starting at 220ms — after the sheet has finished sliding in. Caps at ~10 children so bulk lists don't animate one-by-one. |
| Reduced motion | prefers-reduced-motion: reduce disables the cascade. The slide still fires (short + purposeful). |
Recipes
Ready-to-copy compositions covering the most common Yuno usages of this atom.
The most common Sheet pattern in the Yuno Dashboard: side='right' with a form in the body and Cancel + Save CTAs in the footer. Body scrolls independently so long forms don't blow out the layout.
<Sheet>
<SheetTrigger asChild>
<Button>Open</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Sheet title</SheetTitle>
</SheetHeader>
<SheetBody>
<div className="grid gap-4">
<div className="grid gap-1.5">
<Label htmlFor="field-1">Label</Label>
<Input id="field-1" placeholder="Value" />
</div>
<div className="grid gap-1.5">
<Label htmlFor="field-2">Notes</Label>
<Textarea id="field-2" rows={4} />
</div>
</div>
</SheetBody>
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Cancel</Button>
</SheetClose>
<Button>Save</Button>
</SheetFooter>
</SheetContent>
</Sheet>A back arrow in the header lets the user step out of a sub-view without closing the whole sheet. Common in multi-step editors. Compose it as an icon Button on the left of SheetHeader.
<SheetContent side="right">
<SheetHeader className="flex-row items-center gap-3">
<Button variant="ghost" size="icon" className="-ml-2 size-8">
<ArrowLeft weight="light" className="size-4" />
</Button>
<SheetTitle>Step title</SheetTitle>
</SheetHeader>
<SheetBody>{/* step content */}</SheetBody>
</SheetContent>side='left' for a filter drawer that lives on the same side as the table's row headers. Same Header + Body + Footer shape — only the entry direction changes.
<SheetContent side="left">
<SheetHeader>
<SheetTitle>Filters</SheetTitle>
</SheetHeader>
<SheetBody>{/* filter controls */}</SheetBody>
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Clear</Button>
</SheetClose>
<Button>Apply</Button>
</SheetFooter>
</SheetContent>side='bottom' picks up the rounded-t-3xl Yuno mobile treatment automatically. Use for confirmation-style flows on mobile prototypes where a full modal would be too heavy.
<SheetContent side="bottom">
<SheetHeader>
<SheetTitle>Confirm?</SheetTitle>
</SheetHeader>
<SheetBody>{/* summary */}</SheetBody>
<SheetFooter>
<SheetClose asChild>
<Button variant="outline">Cancel</Button>
</SheetClose>
<Button>Confirm</Button>
</SheetFooter>
</SheetContent>For status-carrying Sheets (Info / Warning contexts), place a Phosphor icon (weight='light') to the left of the title inside the SheetHeader row. Use text-primary for informational, text-destructive for warnings. Do NOT tint the whole Sheet — the icon is enough. Same 'flex-row items-center gap-3' pattern as the back-button recipe.
<SheetContent side="right">
<SheetHeader className="flex-row items-center gap-3">
<Info weight="light" className="size-4 shrink-0 text-primary" />
<SheetTitle>Read-only mode</SheetTitle>
</SheetHeader>
<SheetBody>
<p className="text-sm text-foreground/85">
Your current role can view but not edit this workspace.
</p>
</SheetBody>
<SheetFooter>
<SheetClose asChild><Button>Got it</Button></SheetClose>
</SheetFooter>
</SheetContent>For provider-scoped Sheets (Configure Stripe connection, Edit Adyen route, etc.), load the provider logo from the Yuno CDN and place it inline with the title. Sized at size-6 (24px, bigger than a Phosphor icon because logos are brand marks). CDN pattern: https://icons.prod.y.uno/{slug}_logosimbolo.png. Verified slugs: stripe / adyen / dlocal / payu / mercadopago / 2c2p / visa / mastercard / amex. Never tint the logo.
<SheetContent side="right">
<SheetHeader className="flex-row items-center gap-3">
{/* Provider logo from Yuno CDN */}
<img
src="https://icons.prod.y.uno/stripe_logosimbolo.png"
alt="Stripe"
className="size-6 shrink-0 object-contain"
/>
<SheetTitle>Configure Stripe connection</SheetTitle>
</SheetHeader>
<SheetBody>{/* form fields */}</SheetBody>
<SheetFooter>
<SheetClose asChild><Button variant="outline">Cancel</Button></SheetClose>
<Button>Save changes</Button>
</SheetFooter>
</SheetContent>Adding a SheetDescription is supported but rare — the Dashboard convention is title-only in the header. Reach for a description only when the title alone doesn't carry the context.
<SheetHeader>
<SheetTitle>Sheet title</SheetTitle>
<SheetDescription>
Short helper text that complements the title.
</SheetDescription>
</SheetHeader>Import
Full compound import from @/components/ui/sheet — includes the new SheetBody wrapper.
import {
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";Props
SheetContent and the compound parts forward Radix props. The most useful ones for prototypes:
| Prop | Type | Default | Description |
|---|---|---|---|
| Sheet.open / onOpenChange | boolean / (open: boolean) => void | — | Controlled open state. Omit both for uncontrolled — Radix handles it. Use for programmatic open (from a menu action, for example). |
| SheetTrigger.asChild | boolean | false | Merges Radix's trigger props onto the child element instead of rendering a button. Standard shadcn pattern for using your own Button. |
| SheetContent.side | "top" | "right" | "bottom" | "left" | "right" | Anchor edge. Right is canonical for detail editing. Right / Left = w-full min-w-80 max-w-3xl (defaults to 768px on desktop). Top / Bottom = fill the width; bottom picks up rounded-t-3xl automatically. |
| SheetContent.className | string | — | Merged onto the panel. Use for width overrides (e.g. sm:max-w-md for a slimmer sheet). Don't override the padding — SheetHeader / SheetBody / SheetFooter handle it. |
| SheetHeader | component | — | The bordered top row. Ships p-6 border-b flex-col gap-1.5 text-left with pr-14 reserved for the close X. Contains SheetTitle + optional SheetDescription. |
| SheetBody | component | — | The scrollable middle region. Ships flex-1 overflow-auto p-6. Fills the space between Header and Footer. Wrap long content here so scroll stays inside the sheet. |
| SheetFooter | component | — | The bordered bottom row. Ships p-6 border-t flex-col-reverse gap-2 sm:flex-row sm:justify-end. Primary CTA goes on the right (desktop); on mobile buttons stack full-width with primary on top. Footer buttons are always default size. |
| SheetTitle | component | — | Required by Radix for accessibility. text-lg font-semibold. Keep short — the header truncates. |
| SheetDescription | component | — | Optional and rare in the Dashboard. text-sm text-muted-foreground. Only add when the title alone doesn't carry the context. |
| SheetClose.asChild | boolean | false | Merges close behavior onto your own Button. Standard pattern for the Cancel button in the footer. |
When to use
- Focused edits or quick create forms without leaving the current page.
- Filter or configuration panels that need more room than a Popover.
- Detail views that would be too heavy as a full page but too much for a Popover.
- Multi-step step-in flows where a back arrow leads to a nested sub-view inside the sheet.
When not to use
- Critical yes/no confirmations — use Dialog (or AlertDialog for destructive).
- Transient feedback — use Toast.
- Persistent inline state that shouldn't be dismissable by clicking outside — use an inline Card / Panel.
- Full-page workflows — those belong on a route, not in a sheet.
Usage
- Anchor a primary CTA in the footer (right-aligned on desktop, full-width stack on mobile).
- Use side='right' for creating and detail editing — matches the reading direction and the Dashboard convention.
- Wrap long forms in <SheetBody> so scrolling stays inside the sheet.
- Keep the header title short (≤ 40 chars) — it truncates otherwise.
- Override the width via className when a screen genuinely needs more room (e.g. sm:max-w-2xl) — bounded still by max-w-3xl.
- Don't stack Sheets — one at a time. Nest a step-in view inside the same sheet using the back button.
- Don't hide the close X — it's the primary escape hatch.
- Don't use it for destructive confirmations — use AlertDialog.
- Don't put critical state that must persist inside a Sheet — it's a transient surface.
- Don't override the width past max-w-3xl (768px) — beyond that it stops feeling like a sheet.
Related
Cross-links to atoms and patterns you may reach for next.
- DialogReach for Dialog when the user must decide right now — Sheets are for creating and editing, Dialogs are for deciding.
- DrawerBottom-anchored variant with a drag handle. Use Drawer for mobile-only bottom sheets; use Sheet for anything that lives on desktop.
- PopoverReach for Popover when the surface is small and inline. Sheets take over an edge; Popovers hover next to their trigger.
- Alert dialogFor destructive confirmations — never route those into a Sheet.
- Scroll areaWrap long body content in ScrollArea if you want the Yuno-token scrollbar instead of the native one.