HextaUI

Sheet

Um painel que desliza a partir de qualquer borda, com deslize para dispensar, bloqueio de rolagem e aninhamento empilhado.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetDemo() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Edit profile
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit profile</SheetTitle>
          <SheetDescription>
            Changes are saved to your account when you click save.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <div className="flex flex-col gap-4">
            <div className="flex flex-col gap-1.5">
              <label htmlFor="sheet-demo-name" className="text-sm font-medium">
                Name
              </label>
              <input
                id="sheet-demo-name"
                defaultValue="Olivia Martin"
                className="h-9 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
              />
            </div>
            <div className="flex flex-col gap-1.5">
              <label
                htmlFor="sheet-demo-username"
                className="text-sm font-medium"
              >
                Username
              </label>
              <input
                id="sheet-demo-username"
                defaultValue="@olivia"
                className="h-9 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
              />
            </div>
          </div>
        </SheetBody>
        <SheetFooter>
          <SheetClose render={<Button variant="outline" />}>Cancel</SheetClose>
          <SheetClose render={<Button />}>Save changes</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/sheet.json

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"
<Sheet>
  <SheetTrigger render={<Button variant="outline" />}>Open</SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Edit profile</SheetTitle>
      <SheetDescription>Make changes to your profile.</SheetDescription>
    </SheetHeader>
    <SheetBody>{/* content */}</SheetBody>
    <SheetFooter>
      <SheetClose render={<Button />}>Save changes</SheetClose>
    </SheetFooter>
  </SheetContent>
</Sheet>
Sheet
├── SheetTrigger
└── SheetContent
    ├── SheetHeader
    │   ├── SheetTitle
    │   └── SheetDescription
    ├── SheetBody
    └── SheetFooter
        └── SheetClose

Lados

Defina side para deslizar a partir de qualquer borda. Cada sheet pode ser deslizado de volta para a própria borda, e o gesto de fechar segue seu dedo com inércia.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

const sides = ["top", "right", "bottom", "left"] as const

export function SheetSides() {
  return (
    <div className="grid grid-cols-2 gap-2">
      {sides.map((side) => (
        <Sheet key={side}>
          <SheetTrigger render={<Button variant="outline" />}>
            {side}
          </SheetTrigger>
          <SheetContent side={side}>
            <SheetHeader>
              <SheetTitle>Opens from the {side}</SheetTitle>
              <SheetDescription>
                Swipe towards the {side} edge or press Esc to close it.
              </SheetDescription>
            </SheetHeader>
            <SheetFooter>
              <SheetClose render={<Button />}>Done</SheetClose>
            </SheetFooter>
          </SheetContent>
        </Sheet>
      ))}
    </div>
  )
}

Conteúdo longo

<SheetBody /> rola por conta própria, então o cabeçalho e o rodapé ficam no lugar por mais longo que o conteúdo seja.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetLongContent() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Release notes
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>What’s new</SheetTitle>
          <SheetDescription>Version 2.4.0</SheetDescription>
        </SheetHeader>
        <SheetBody>
          <div className="flex flex-col gap-3 text-sm text-muted-foreground">
            {Array.from({ length: 24 }, (_, index) => (
              <p key={index}>
                {index + 1}. Improved performance of the dashboard charts and
                fixed an issue where filters reset after navigation.
              </p>
            ))}
          </div>
        </SheetBody>
        <SheetFooter>
          <SheetClose render={<Button />}>Got it</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}

Sem o botão de fechar

Defina showCloseButton={false} quando o rodapé já tem uma saída. Escape, o backdrop e o gesto de deslizar ainda o fecham.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetNoCloseButton() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>Share</SheetTrigger>
      <SheetContent side="bottom" showCloseButton={false}>
        <SheetHeader>
          <SheetTitle>Share link</SheetTitle>
          <SheetDescription>
            Anyone with the link can view this document.
          </SheetDescription>
        </SheetHeader>
        <SheetFooter>
          <SheetClose render={<Button />}>Done</SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  )
}

Aninhado

Um sheet ou alert dialog aberto a partir de um sheet fica em camada por cima. O pai se reduz levemente e cada camada extra adiciona um backdrop mais claro, para a pilha continuar legível. Escape fecha apenas a camada do topo.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetNested() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Workspace settings
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Workspace settings</SheetTitle>
          <SheetDescription>
            Manage members and the danger zone.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <div className="flex flex-col items-start gap-2">
            <Sheet>
              <SheetTrigger render={<Button variant="outline" />}>
                Manage members
              </SheetTrigger>
              <SheetContent>
                <SheetHeader>
                  <SheetTitle>Members</SheetTitle>
                  <SheetDescription>
                    The parent sheet steps back while this one is open.
                  </SheetDescription>
                </SheetHeader>
              </SheetContent>
            </Sheet>
            <AlertDialog>
              <AlertDialogTrigger render={<Button variant="destructive" />}>
                Delete workspace
              </AlertDialogTrigger>
              <AlertDialogContent>
                <AlertDialogHeader>
                  <AlertDialogTitle>Delete workspace?</AlertDialogTitle>
                  <AlertDialogDescription>
                    All projects in this workspace will be removed.
                  </AlertDialogDescription>
                </AlertDialogHeader>
                <AlertDialogFooter>
                  <AlertDialogCancel>Cancel</AlertDialogCancel>
                  <AlertDialogAction variant="destructive">
                    Delete
                  </AlertDialogAction>
                </AlertDialogFooter>
              </AlertDialogContent>
            </AlertDialog>
          </div>
        </SheetBody>
      </SheetContent>
    </Sheet>
  )
}

Controlado

Passe open e onOpenChange para abri-lo de qualquer lugar, sem gatilho.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetBody,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
} from "@/components/ui/sheet"

export function SheetControlled() {
  const [open, setOpen] = React.useState(false)

  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open from state
      </Button>
      <span className="text-sm text-muted-foreground">
        {open ? "Open" : "Closed"}
      </span>
      <Sheet open={open} onOpenChange={setOpen}>
        <SheetContent>
          <SheetHeader>
            <SheetTitle>Edit profile</SheetTitle>
            <SheetDescription>
              Changes are saved to your account when you click save.
            </SheetDescription>
          </SheetHeader>
          <SheetBody>
            <div className="flex flex-col gap-4">
              <div className="flex flex-col gap-1.5">
                <label
                  htmlFor="sheet-controlled-name"
                  className="text-sm font-medium"
                >
                  Name
                </label>
                <input
                  id="sheet-controlled-name"
                  defaultValue="Olivia Martin"
                  className="h-9 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
                />
              </div>
              <div className="flex flex-col gap-1.5">
                <label
                  htmlFor="sheet-controlled-username"
                  className="text-sm font-medium"
                >
                  Username
                </label>
                <input
                  id="sheet-controlled-username"
                  defaultValue="@olivia"
                  className="h-9 rounded-md border border-input bg-transparent px-3 text-sm transition-shadow outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden pointer-coarse:text-touch"
                />
              </div>
            </div>
          </SheetBody>
          <SheetFooter>
            <SheetClose render={<Button variant="outline" />}>
              Cancel
            </SheetClose>
            <SheetClose render={<Button />}>Save changes</SheetClose>
          </SheetFooter>
        </SheetContent>
      </Sheet>
    </div>
  )
}

Gatilhos desanexados

Compartilhe um sheet entre vários gatilhos com createSheetHandle. Cada gatilho passa um payload que o sheet renderiza por meio de um filho em função.

"use client"

import { Button } from "@/components/ui/button"
import {
  createSheetHandle,
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

const orders = createSheetHandle<{ id: string; status: string }>()

export function SheetDetached() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <SheetTrigger
        handle={orders}
        payload={{ id: "#1042", status: "Shipped" }}
        render={<Button variant="outline" size="sm" />}
      >
        Order #1042
      </SheetTrigger>
      <SheetTrigger
        handle={orders}
        payload={{ id: "#1043", status: "Processing" }}
        render={<Button variant="outline" size="sm" />}
      >
        Order #1043
      </SheetTrigger>
      <Sheet handle={orders}>
        {({ payload }) => (
          <SheetContent>
            <SheetHeader>
              <SheetTitle>Order {payload?.id}</SheetTitle>
              <SheetDescription>Status: {payload?.status}</SheetDescription>
            </SheetHeader>
          </SheetContent>
        )}
      </Sheet>
    </div>
  )
}

Da direita para a esquerda

Com dir="rtl", o lado right se espelha para a borda esquerda, desliza a partir dela e sai deslizando pelo mesmo lado.

import { Button } from "@/components/ui/button"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function SheetRtl() {
  return (
    <div dir="rtl">
      <Sheet>
        <SheetTrigger render={<Button variant="outline" />}>افتح</SheetTrigger>
        <SheetContent dir="rtl">
          <SheetHeader>
            <SheetTitle>تعديل الملف الشخصي</SheetTitle>
            <SheetDescription>
              يتم حفظ التغييرات عند النقر على حفظ.
            </SheetDescription>
          </SheetHeader>
          <SheetFooter>
            <SheetClose render={<Button variant="outline" />}>إلغاء</SheetClose>
            <SheetClose render={<Button />}>حفظ</SheetClose>
          </SheetFooter>
        </SheetContent>
      </Sheet>
    </div>
  )
}
TeclaAção
EnterSpaceNo gatilho, abre o sheet e move o foco para dentro dele.
TabShift + TabMove entre elementos focáveis. O foco permanece dentro do sheet.
EscFecha o sheet do topo e devolve o foco ao seu gatilho.
  • O sheet é um dialog modal. <SheetTitle /> o rotula e <SheetDescription /> o descreve, então sempre inclua um título.
  • A rolagem da página é travada e o conteúdo atrás dele fica oculto das tecnologias assistivas enquanto está aberto.
  • O botão de fechar é rotulado “Close”. O handle na borda interna mostra que o sheet pode ser arrastado e fica oculto das tecnologias assistivas.
  • Com movimento reduzido ativado, o sheet aparece e some com fade em vez de deslizar.

Construído sobre o drawer do Base UI. Cada parte aceita as props da primitiva que envolve.

PropTipoPadrão
defaultOpen
booleanfalse
open
boolean–
onOpenChange
(open: boolean, details) => void–
onOpenChangeCompleteChamado após o fim da animação de abertura ou fechamento.
(open: boolean) => void–
modal
boolean | "trap-focus"true
disablePointerDismissalMantém aberto quando o backdrop é clicado.
booleanfalse
swipeDirectionDefinido automaticamente a partir do lado e da direção do conteúdo.
"up" | "down" | "left" | "right"–
handle
SheetHandle<Payload>–
children
ReactNode | ({ payload }) => ReactNode–
PropTipoPadrão
handle
SheetHandle<Payload>–
payload
Payload–
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescrição
data-slot="sheet-trigger"Selecione o gatilho no CSS.
data-popup-openPresente enquanto seu sheet está aberto.

Renderiza o portal, o backdrop, o viewport e o popup, além do handle de arrastar e do botão de fechar.

PropTipoPadrão
side
"top" | "right" | "bottom" | "left""right"
showCloseButton
booleantrue
dirDefine a direção do sheet. right e left espelham em rtl.
"ltr" | "rtl"–
initialFocus
boolean | RefObject | (type) => HTMLElement | boolean–
finalFocus
boolean | RefObject | (type) => HTMLElement | boolean–
classNameOs sheets laterais têm 75% de largura, até 24rem a partir do breakpoint sm.
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="sheet-content"O painel do sheet.
data-sideO lado de onde ele abre.
data-openPresente enquanto o sheet está aberto.
data-starting-stylePresente enquanto a animação de entrada ocorre.
data-ending-stylePresente enquanto a animação de saída ocorre.
data-swipingPresente enquanto está sendo arrastado.
data-swipe-directionA direção que o dispensa.
data-nested-drawer-openPresente enquanto um sheet aninhado está aberto por cima.
data-slot="sheet-handle"O handle de arrastar na borda interna.
data-slot="sheet-close-button"O botão de fechar embutido.
--drawer-swipe-movement-xDistância de arraste horizontal. Também existe uma variável -y.
--nested-drawersQuantos sheets aninhados estão abertos por cima.

Renderizado por <SheetContent />. Camadas aninhadas recebem um backdrop mais claro.

AtributoDescrição
data-slot="sheet-overlay"O pano de fundo.
data-nestedPresente nos backdrops de camadas aninhadas.

Elementos <div> simples que organizam o sheet. O cabeçalho deixa espaço para o botão de fechar, o corpo rola e ocupa a altura restante, e o rodapé empilha suas ações em telas pequenas e as alinha ao final a partir do breakpoint sm.

AtributoDescrição
data-slot="sheet-header"Título e descrição.
data-slot="sheet-body"Conteúdo rolável.
data-slot="sheet-footer"Ações.
PropTipoPadrão
render
ReactElement | (props, state) => ReactElement<h2>
AtributoDescrição
data-slot="sheet-title"Rotula o sheet.
PropTipoPadrão
render
ReactElement | (props, state) => ReactElement<p>
AtributoDescrição
data-slot="sheet-description"Descreve o sheet.
PropTipoPadrão
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescrição
data-slot="sheet-close"Fecha o sheet quando pressionado.

createSheetHandle<Payload>() retorna um handle que conecta um <Sheet /> a gatilhos renderizados em outro lugar. Crie-o uma vez, fora do seu componente.

Usado em blocos

Blocos que se baseiam em Sheet.