Inkbox

Component

Sheet

A white panel that slides from an edge over the blurred, ink-tinted page, with the dialog's title and close glyph.

npx -y github:anwarlemu/inkbox-brand-cli#release add sheet
Preview
Sides
Open

Usage

Use for a task beside the page that keeps its context, such as editing a mailbox or reading a call's details. Use side="right" by default and side="left" for the rail on phones. Put the title in SheetHeader and the main action in SheetFooter.

Avoid

  • A sheet that opens another sheet
  • A dark scrim
  • A sheet for a short confirmation (use Alert Dialog)

Exports: Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger. This component is a client component.

Dependencies: surface, radix-ui, @phosphor-icons/react.

Example

examples/sheet.tsx
"use client";​import * as React from "react";​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";​function EditMailbox() {  return (    <>      <SheetHeader>        <SheetTitle>Edit mailbox</SheetTitle>        <SheetDescription>Mailboxes are provisioned and deleted with their owning identity.</SheetDescription>      </SheetHeader>      <div className="grid gap-4 px-6">        <div className="grid gap-1">          <Label htmlFor="sheet-name">Display name</Label>          <Input id="sheet-name" defaultValue="Acme support" />        </div>        <div className="grid gap-1">          <Label htmlFor="sheet-reply">Reply-to address</Label>          <Input id="sheet-reply" type="email" placeholder="support@acme.com" />        </div>      </div>      <SheetFooter>        <Button>Save changes</Button>        <SheetClose asChild>          <Button variant="secondary">Cancel</Button>        </SheetClose>      </SheetFooter>    </>  );}​export function Preview() {  return (    <div className="flex justify-center">      <Sheet>        <SheetTrigger asChild>          <Button variant="outline">Edit mailbox</Button>        </SheetTrigger>        <SheetContent>          <EditMailbox />        </SheetContent>      </Sheet>    </div>  );}​export function Sides() {  const sides = ["top", "right", "bottom", "left"] as const;  return (    <div className="flex flex-wrap justify-center gap-3">      {sides.map((side) => (        <Sheet key={side}>          <SheetTrigger asChild>            <Button variant="outline" className="capitalize">              {side}            </Button>          </SheetTrigger>          <SheetContent side={side}>            <SheetHeader>              <SheetTitle>Sheet from the {side}</SheetTitle>              <SheetDescription>Slides from its edge on the drawer curve.</SheetDescription>            </SheetHeader>          </SheetContent>        </Sheet>      ))}    </div>  );}​export function Open() {  return (    <div className="min-h-120">      <Sheet defaultOpen>        <SheetContent>          <EditMailbox />        </SheetContent>      </Sheet>    </div>  );}

Source

components/ui/sheet.tsx
"use client";​import * as React from "react";import { Dialog as SheetPrimitive } from "radix-ui";import { XIcon } from "@phosphor-icons/react/ssr";​import { usePortalContainer } from "@/lib/surface";import { cn } from "@/lib/utils";​// A panel from the edge of the screen, drawn as the console's dialog (Figma 427:2715: Create Agent Identity): white// over the blurred, ink-tinted page, a 20px regular title 24px in, a faint 20px close glyph and the same 24px sides. It// travels its own size from its edge on the drawer curve (300ms in, 200ms out); its inner corner takes the dialog's// 16px. Side panels are 384px wide (the full width on phones); top and bottom panels take their content's height.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>) {  const container = usePortalContainer();  return <SheetPrimitive.Portal data-slot="sheet-portal" container={container} {...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-scrim backdrop-blur-[var(--ds-blur-scrim)] data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in", className)}      {...props}    />  );}​const SIDES = {  right: "inset-y-0 right-0 h-full w-full rounded-l-dialog sm:max-w-96 data-[state=closed]:animate-sheet-out-right data-[state=open]:animate-sheet-in-right",  left: "inset-y-0 left-0 h-full w-full rounded-r-dialog sm:max-w-96 data-[state=closed]:animate-sheet-out-left data-[state=open]:animate-sheet-in-left",  top: "inset-x-0 top-0 h-auto max-h-[85dvh] rounded-b-dialog data-[state=closed]:animate-sheet-out-top data-[state=open]:animate-sheet-in-top",  bottom: "inset-x-0 bottom-0 h-auto max-h-[85dvh] rounded-t-dialog data-[state=closed]:animate-sheet-out-bottom data-[state=open]:animate-sheet-in-bottom",} as const;​function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }: React.ComponentProps<typeof SheetPrimitive.Content> & { side?: keyof typeof SIDES; showCloseButton?: boolean }) {  return (    <SheetPortal>      <SheetOverlay />      <SheetPrimitive.Content        data-slot="sheet-content"        data-side={side}        className={cn("fixed z-50 flex flex-col overflow-y-auto bg-surface text-fg shadow-dialog outline-none", SIDES[side], className)}        {...props}      >        {children}        {showCloseButton && (          <SheetPrimitive.Close            data-slot="sheet-close"            className="absolute top-5 right-5 inline-flex size-7 items-center justify-center rounded-control text-fg-faint outline-none transition-colors duration-150 ease-out hover:text-fg-muted focus-visible:outline-2 focus-visible:outline-focus-ring motion-reduce:transition-none"          >            <XIcon className="size-5.5" />            <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 min-h-17 shrink-0 flex-col justify-center gap-1 py-5 pr-14 pl-6", className)} {...props} />;}​function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {  return <div data-slot="sheet-footer" className={cn("mt-auto flex shrink-0 flex-col gap-3 p-6", className)} {...props} />;}​function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {  return <SheetPrimitive.Title data-slot="sheet-title" className={cn("font-sans text-h3 leading-heading font-normal text-fg", className)} {...props} />;}​function SheetDescription({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Description>) {  return <SheetPrimitive.Description data-slot="sheet-description" className={cn("text-body leading-body text-fg-secondary", className)} {...props} />;}​export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger };

Reference: Extrapolated from the Inkbox console (Figma 427:2715): Modal - Create Agent (panel, title, close glyph, scrim).