shadimage

sheet

registry/pouf/sheet.tsx

import * as RDialog from '@radix-ui/react-dialog'
import type { ReactNode } from 'react'
import { Button } from './Button'
import { Icon } from './Icon'
import { Stack } from './layout'
import { Heading, Text } from './text'

interface SheetProps {
  /** Omit for a fully controlled sheet (open/onOpenChange) with no in-place
   *  trigger — e.g. opened from a table row click. */
  trigger?: ReactNode
  title: string
  description?: string
  children: ReactNode
  open?: boolean
  onOpenChange?: (open: boolean) => void
}

/** A side sheet — NOT a centred dialog.
 *
 * The old Sheet reused .pouf-dialog, so "Open sheet" produced something visually
 * identical to a modal. A sheet reads as a panel that slides in from an edge: the
 * right on desktop, the bottom on a phone (where the thumb is). Enter and exit are
 * CSS animations keyed off Radix's data-state — Radix's Presence keeps the node
 * mounted until the close animation ends, so it slides out too, no framer needed.
 */
export function Sheet({ trigger, title, description, children, open, onOpenChange }: SheetProps) {
  return (
    <RDialog.Root open={open} onOpenChange={onOpenChange}>
      {trigger && <RDialog.Trigger asChild>{trigger}</RDialog.Trigger>}
      <RDialog.Portal>
        <RDialog.Overlay className="pouf-overlay" />
        <RDialog.Content className="pouf-sheet-dialog">
          <div className="pouf-sheet-panel">
            <div className="pouf-dialog__head">
              <Stack gap={1}>
                <RDialog.Title asChild>
                  <div>
                    <Heading level={3}>{title}</Heading>
                  </div>
                </RDialog.Title>
                {description && (
                  <RDialog.Description asChild>
                    <div>
                      <Text size="sm" muted>
                        {description}
                      </Text>
                    </div>
                  </RDialog.Description>
                )}
              </Stack>
              <RDialog.Close asChild>
                <Button variant="quiet" size="sm" label="Close">
                  <Icon name="close" size="sm" />
                </Button>
              </RDialog.Close>
            </div>
            <div className="pouf-dialog__body">{children}</div>
          </div>
        </RDialog.Content>
      </RDialog.Portal>
    </RDialog.Root>
  )
}

No selection tags yet — empty means no evidence, not lack of support.

DependenciesBundle weight · light

ui-primitives
@radix-ui/react-dialog