Skip to Content
ComponentsOrganismsResponsive sheet

Responsive Sheet

Organism

Mesma intenção, superfície diferente: side sheet no desktop, bottom sheet no mobile.

Use quando o conteúdo precisa continuar conectado ao item de origem: detalhe de menção, filtros avançados, edição contextual, Cris embedding, handover ou ação assistida.

Exemplos copiáveis

React

import * as React from "react" type PulsoResponsiveSheetProps = { open: boolean title: string description?: string children: React.ReactNode controls?: React.ReactNode onOpenChange: (open: boolean) => void } export function PulsoResponsiveSheet({ open, title, description, children, controls, onOpenChange }: PulsoResponsiveSheetProps) { const titleId = React.useId() const descriptionId = description ? `${titleId}-description` : undefined React.useEffect(() => { if (!open) return function onKeyDown(event: KeyboardEvent) { if (event.key === "Escape") onOpenChange(false) } document.addEventListener("keydown", onKeyDown) return () => document.removeEventListener("keydown", onKeyDown) }, [open, onOpenChange]) if (!open) return null return ( <div className="fixed inset-0 z-50 bg-black/35" role="presentation"> <aside role="dialog" aria-modal="true" aria-labelledby={titleId} aria-describedby={descriptionId} className="fixed inset-x-0 bottom-0 max-h-[88dvh] overflow-hidden rounded-t-[var(--radius-xl)] border border-[var(--border)] bg-[var(--popover)] shadow-[var(--shadow-lg)] motion-safe:duration-[var(--motion-duration-base)] md:inset-y-0 md:left-auto md:w-[min(480px,calc(100vw-3rem))] md:max-h-none md:rounded-none md:rounded-l-[var(--radius-xl)]"> <div className="mx-auto mt-3 h-1.5 w-10 rounded-full bg-[var(--border-strong)] md:hidden" /> <header className="flex items-start justify-between gap-4 border-b border-[var(--border)] p-5"> <div> <h2 id={titleId} className="text-lg font-semibold text-[var(--fg)]">{title}</h2> {description ? <p id={descriptionId} className="mt-1 text-sm leading-6 text-[var(--fg-muted)]">{description}</p> : null} </div> <button type="button" aria-label="Fechar" onClick={() => onOpenChange(false)} className="rounded-[var(--radius-sm)] px-2 py-1 text-[var(--fg-muted)] hover:bg-[var(--muted)]">×</button> </header> <div className="max-h-[calc(88dvh-9rem)] overflow-y-auto p-5 md:max-h-[calc(100dvh-9rem)]">{children}</div> {controls ? <footer className="border-t border-[var(--border)] p-4"><div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">{controls}</div></footer> : null} </aside> </div> ) }

Composição interna (Padrão 8)

O Sheet tem três regiões com papéis fixos:

RegiãoRegra
Headerfixo; kicker + título + descrição — o conteúdo não repete esse título
Conteúdoscrollável (min-h-0 flex-1); seções separadas por divisor + espaço em branco, nunca por caixas com borda
Footerfixo; ação principal à esquerda

Regra de contenção única: dentro de um Sheet, no máximo um nível de contenção visual (tint suave sem borda, ex. bg-muted/10) — nunca bloco com borda dentro de bloco com borda. Hierarquia interna se faz com kicker/título/divisor, não com molduras. Métricas resumidas viram linhas de definição (kicker em cima, valor embaixo), não grade de caixinhas.

Anti-padrão: repetir o título do Sheet numa “caixa-mãe” interna que envolve todo o conteúdo — o header já cumpre esse papel.

Regras

  1. Não use Responsive Sheet para alerta bloqueante; use Dialog.
  2. Adapte superfície, não duplique conteúdo entre desktop e mobile.
  3. Side sheet deve preservar contexto da lista por trás.
  4. Bottom sheet deve respeitar polegar, gesto de fechamento e scroll interno.
  5. Conteúdo assistido por IA precisa manter rastreabilidade visível.
  6. Sheet nunca abre outra Sheet (sem empilhamento); ações destrutivas em DropdownMenu, não expostas.
Last updated on