Uma grade de itens e um painel só. No clique, o painel entra deslizando da borda direita e ocupa uma faixa da tela — 520px no desktop, largura cheia no celular —, com o resto da página escurecido e desfocado atrás. Cabeçalho e rodapé ficam parados; o corpo rola por dentro, com o corte indicado por máscara em gradiente. O olho não muda de página: ele ganha uma segunda coluna que aparece por cima da primeira.
O problema editorial é o do detalhe que não cabe no card e não merece uma página. Trajetória de um assessor, escopo de um serviço, o que acontece numa etapa do processo, a linha de uma tabela: são 150 a 300 palavras que ninguém vai procurar num link, e que estufam a grade se ficarem soltas nela. O drawer as guarda até serem pedidas e devolve a pessoa exatamente onde ela estava — que é a diferença inteira para navegar.
Quando usar
- Grade, lista ou tabela de 3 a 12 itens comparáveis, em que cada item tem um detalhe de 150 a 300 palavras.
- Quando manter o lugar na página importa: a pessoa está comparando itens, e mandá-la para outra URL a faz perder a comparação e a rolagem.
- Quando o detalhe termina numa ação — falar com alguém, pedir o material, agendar. O rodapé parado do painel existe para isso, e é o que transforma a peça em conversão e não só em leitura.
- Equipe de assessoria, catálogo de serviços, etapas de um método, glossário de termos regulados.
Quando não usar
- Detalhe com mais de ~400 palavras, foto grande ou vídeo. Faixa de 520px estrangula esse conteúdo. Ali o item merece uma página, ou o
overlay/card-expansivel-fullscreen, que abre em tela cheia e é feito para o detalhe com imagem. - Menu de navegação. É o
navegacao/drawer-fullscreen— ver abaixo. - Uma pergunta e uma resposta curta. Um painel de 100vh para três linhas é um canhão. Use
listas/acordeao-faq. - Item único, sem irmãos. Sem grade não há o que comparar, e o custo do painel não se paga: o texto pode simplesmente estar na página.
- Formulário. Campo dentro de overlay com rolagem própria e teclado virtual é armadilha em celular.
conversao/cta-formulario-leadresolve na página.
Este ou o navegacao/drawer-fullscreen?
Os dois deslizam da direita e os dois se chamam drawer, e é onde a semelhança acaba.
O navegacao/drawer-fullscreen é menu. Cobre a tela inteira e opaca, tem conteúdo fixo — os mesmos cinco links e o mesmo CTA, sempre —, é aberto pelo hambúrguer da nav e existe só no celular. O que ele faz é levar a pessoa embora: todo clique lá dentro fecha o painel e navega. Como não há nada por trás para se ver, cobrir tudo é o certo.
O overlay/drawer-lateral é conteúdo. Ocupa uma faixa e deixa a página visível atrás de propósito, porque o contexto — a grade de onde o item saiu — é metade da informação. O conteúdo dele troca a cada item, e o painel é um só para todos os gatilhos. Existe em todas as larguras. O que ele faz é o oposto do menu: devolver a pessoa ao mesmo ponto.
Regra curta: navegar é drawer-fullscreen; detalhar é drawer-lateral. Se um dia a nav precisar de um painel que mostre a página por trás, a variação é do menu, não deste. E se o detalhe pedir a tela inteira com foto, não é nenhum dos dois: é overlay/card-expansivel-fullscreen.
Nenhum componente catalogado resolvia isto antes: o card-expansivel-fullscreen cobre a tela e carrega o detalhe nos data-* do card (bom para texto curto, ruim para prosa com listas e ressalva); o drawer-fullscreen é menu; e o conteudo/grid-equipe mostra a grade mas não tem para onde abrir. Este componente substitui o card-expansivel-fullscreen quando o detalhe é texto estruturado e o contexto precisa continuar visível — e é o par natural do grid-equipe, que ele pode substituir por inteiro quando cada pessoa tiver perfil para ler.
Padrão editorial
| Slot | Regra |
|---|---|
| Olho | Rótulo da coleção, não do item. "Quem atende", "Serviços", "Etapas". |
| Título | Frase da dobra, não lista de nomes. Os nomes estão nos cards. |
| Apoio | Uma linha que avisa que abre: "abra uma para ler a trajetória sem sair desta página". Sem isso, metade das pessoas não clica em card. |
| Monograma | Duas letras, iniciais. Placeholder honesto enquanto não há retrato. |
| Nome do card | Até 3 palavras. É o que vira título do painel. |
| Cargo | Uma linha, sem sigla interna da casa. |
| Áreas | Duas ou três, separadas por ·. Caixa alta pequena, é etiqueta e não frase. |
| Rótulo de ação | O mesmo texto nos quatro cards ("Ver trajetória"). Variar por card lê como quatro coisas diferentes. |
| Frase do painel | Uma linha em display, entre aspas, com filete na cor de marca. É a voz da pessoa, não o slogan da empresa. |
| Corpo | Dois parágrafos de trajetória e uma lista de 3 itens de "onde costuma entrar". Mantenha o mesmo comprimento nos quatro: o painel tem altura fixa, e um item muito mais curto que o vizinho denuncia. |
| Ressalva | Sempre a última coisa do corpo, dentro do painel. Nunca no rodapé da página. |
| CTA | Verbo de conversa, nunca de compra. O script troca o primeiro nome sozinho. |
Quatro itens é o número que o desenho pede: a grade é de quatro colunas no desktop, duas em tablet, uma no celular. Oito também fecha (duas fileiras). Três deixa uma coluna órfã — nesse caso troque --drw-w e a contagem de colunas juntos.
Especificação dos assets
O componente não usa asset nenhum no padrão: o monograma é texto, e os ícones são SVG inline de 11px e 16px, desenhados aqui.
Se o cliente tiver retratos e você quiser trocar o monograma por foto:
- Formato e dimensão mínima: quadrado 1:1, 600×600, WebP.
- Peso alvo: até 60 KB por retrato.
- Composição: rosto centrado, do peito para cima, fundo neutro e o mesmo fundo nas quatro fotos. Retratos de estúdios diferentes na mesma grade é o erro mais comum aqui.
- O que não pode ter: recorte em pé inteiro (some no disco de 50px), logo da corretora ao fundo, foto de banco de imagem no lugar de pessoa real da equipe.
Como usar
<link rel="stylesheet" href="componentes/overlay/drawer-lateral/estilo.css">
<script src="componentes/overlay/drawer-lateral/script.js"></script>
O painel é um <dialog> que mora fora da seção, como irmão dela — dentro de uma dobra ele herdaria o escopo de tom, e o §5 do contrato proíbe elemento posicionado em relação à janela dentro de qualquer [data-rv].
<dialog class="dialog-casca drw sec-deep" id="painel-equipe" data-drw aria-label="Detalhe do item"> … </dialog>
.dialog-casca antes da classe local, e ela não é opcional. É a primitiva do core que zera a caixa que o navegador dá ao <dialog> — padding, borda, fundo e os limites de max-content — e pinta o ::backdrop com --veil. Aqui há uma diferença que vale saber: neste componente o <dialog> é o painel, não um palco transparente com um painel dentro, então a superfície (background:var(--bg)) e a tinta (color:var(--fg)) continuam declaradas no estilo.css, por cima do transparent/inherit da primitiva. E o padding tem de continuar zero: é o que faz o clique no fundo ter alvo (ver Gotchas).
Item novo custa dois blocos e nenhuma linha de script:
<!-- 1. o gatilho, dentro da grade -->
<button class="drw-card glass glass-hover" type="button"
data-drw-abrir="painel-equipe" data-drw-fonte="perfil-novo"
aria-haspopup="dialog"> … </button>
<!-- 2. o conteúdo, dentro de .drw-fontes -->
<article class="drw-fonte" id="perfil-novo">
<h4 class="drw-fonte-nome" data-drw-nome>Nome</h4>
<p class="drw-fonte-cargo" data-drw-cargo>Cargo</p>
<div class="drw-fonte-corpo" data-drw-mover> … </div>
</article>
Qualquer elemento da página com [data-drw-abrir] passa a abrir o painel que o valor nomeia — o gatilho não precisa estar na grade nem na seção.
Ganchos
| Gancho | Papel |
|---|---|
[data-drw] |
o <dialog> do painel. Precisa de id. |
[data-drw-abrir] |
gatilho; o valor é o id do painel |
[data-drw-fonte] |
no gatilho; o valor é o id do bloco de conteúdo |
[data-drw-fontes] |
invólucro dos blocos; o script o esconde na última linha |
[data-drw-mover] |
o miolo que viaja para dentro do painel e volta |
[data-drw-nome] / [data-drw-cargo] |
texto copiado para o cabeçalho fixo |
[data-drw-alvo] |
onde o miolo é encaixado dentro do painel |
[data-drw-nome-alvo] / [data-drw-cargo-alvo] / [data-drw-cta-nome] |
destinos do texto copiado |
[data-drw-fechar] |
botão de fechar |
.drw.is-aberto |
estado aberto: deslize e entrada em três faixas |
.drw.is-arrastando |
arrasto de toque em curso; desliga a transição |
--drw-w |
largura do painel no desktop (520px) |
--drw-pad |
respiro interno do painel — não é o --pad do grid |
--drw-dur |
tempo do deslize (.44s); o script tem a mesma constante em ms |
--drw-faixa |
passo do escalonamento entre cabeça, corpo e pé (.09s) |
.dialog-casca |
primitiva do core no <dialog>, antes da classe local: zera a caixa do navegador e pinta o ::backdrop com --veil |
--dialog-blur |
parafuso da primitiva: desfoque do ::backdrop. 7px aqui, contra os 6px do padrão. Declarado no .drw::backdrop, e não no .drw — ver Gotchas |
Variações
- Abrir pela esquerda —
margin:0 auto 0 0etransform:translateX(-100%)no.drw; no script, inverter o sinal do arrasto (Math.min(0, …)e limite negativo). Experimental. - Largura do painel —
--drw-w. 420px para detalhe curto, 680px quando o corpo leva tabela. Experimental. - Sem rodapé — remover o
.drw-pequando a peça é de leitura e não de conversão. O corpo passa a ocupar a altura toda; nada mais muda. Experimental. - Um painel por item — vários
<dialog>, cada um com o conteúdo dentro. Custa markup, duplica a estrutura e perde o estado sem JavaScript (o<dialog>fechado édisplay:nonee nenhum leitor alcança o conteúdo). Só compensa quando cada item tem layout próprio — um com tabela, outro com gráfico. Experimental.
Compliance
A peça não calcula nem compara nada, mas fala em nome de pessoas e termina num CTA, e isso já é terreno regulado. Antes de publicar:
- Nomes e trajetórias do exemplo são fictícios. Helena Duarte, Rafael Sampaio, Marina Bastos e Otávio Lins não existem. Anos de mercado e datas estão marcados como ilustrativos em comentário no markup — troque pelos reais.
- Cargo e credencial. Se a pessoa é assessora de investimentos, o cargo tem de dizer isso e a certificação exigida precisa estar em dia. Não invente título ("head de patrimônio", "estrategista-chefe") que não corresponda ao registro.
- Nada de resultado. O corpo fala de trajetória, área de atuação e método. Nenhuma frase pode citar rentabilidade obtida, comparação com índice ou promessa — nem em depoimento embutido.
- A ressalva mora dentro do painel, como última coisa do corpo, e se repete no rodapé do painel. É onde a pessoa está lendo. Rodapé de página não conta.
- Remuneração declarada. O texto padrão diz "taxa previamente acordada com o cliente". Se o modelo do cliente for outro, o texto muda — e é uma decisão da área de compliance dele, não sua.
- Nada é gravado. O componente não usa
localStorage, cookie ou envio. O CTA aponta para o formulário; a captura é de lá. - Retrato de pessoa real precisa de autorização de uso de imagem por escrito, inclusive de quem já saiu da empresa. É a razão de o padrão vir com monograma.
Gotchas
- O
<dialog>centralizado vira faixa lateral sem!important.position:fixed+inset:0+margin:0 0 0 autocom largura fixa cola o painel na direita;block-size:100%o estica. Trocar porright:0;top:0sem oinsetreintroduz omargin:autodo agente do usuário e o painel volta ao centro. displaysó na regra[open]. O<dialog>nascedisplay:nonena folha do agente do usuário. Escrever.drw{display:flex}deixaria o painel visível o tempo todo, com a página rolando por baixo — a mesma armadilha do §6.5 do contrato, na versão do elemento nativo.- O estado de repouso da entrada é escopado em
.drw, e isso não é enfeite. O corpo do detalhe também vive fora do painel, na página, quando não há script. Sem o.drwna frente, a página sem JavaScript renderizaria os quatro perfis emopacity:0— conteúdo montado, correto e invisível. - Quem esconde os perfis é o script, na última linha dele. Nunca o CSS, nunca o
hiddenno HTML. Um erro em qualquer ponto antes disso deixa a página no estado sem JavaScript, com tudo legível, em vez de vazia..drw-fontes[hidden]{display:none}está no CSS porque odisplay:griddo bloco vence o[hidden]do navegador. - Move, não clona. Clonar duplicaria todo
iddo bloco enquanto o painel está aberto — a começar peloidque o próprio gatilho nomeia. Comidrepetido,getElementByIddevolve a cópia da página em vez da que está na tela e a mesma prosa aparece duas vezes na árvore de acessibilidade. O risco do outro lado (bloco perdido se o script morrer) está fechado por três coisas: a devolução acontece no eventoclose, que é o funil único de todo caminho de fechamento; toda abertura devolve antes de tirar; e o bloco só sai da página depois de tudo ligado. - Os rótulos internos do corpo (
Trajetória,Onde costuma entrar) são parágrafos, não títulos. O mesmo bloco vive em dois contextos com profundidade diferente — abaixo de umh4na página e abaixo doh2do painel — e nenhum nível de heading está certo nos dois. O único heading real é o nome, e o painel o redesenha no cabeçalho dele. - A saída anima antes do
close(). O<dialog>some no mesmo quadro em queclose()é chamado. Esc disparacancelantes declose; barrar o padrão ali é o que dá ao teclado a mesma saída animada do mouse. A espera éSAIDAno script e--drw-durno CSS — mudar um pede mudar o outro. - Trocar de item com o painel já aberto não chama
showModal(). Chamar num<dialog>aberto lançaInvalidStateError. Nesse caminho o elemento também não refaz o foco inicial, então o script manda o foco ao corpo — sem isso o leitor de tela continuaria lendo o item anterior. - O foco passa pelo
<body>ao dar a volta. Medido:fechar → CTA → BODY → fechar. É o ponto de wrap do próprio Chrome para<dialog>modal, não um vazamento — o conteúdo da página estáinerte nunca é alcançado. Não escreva um focus trap por cima disso. touch-action:pan-y pinch-zoomé o que separa o gesto de fechar da rolagem do corpo. O navegador fica com o movimento vertical e entrega ao script só o horizontal; sem isso seria precisopreventDefaultnotouchmove, e a rolagem do corpo morreria junto. Opinch-zoomestá declarado porquepan-ysozinho desligaria o zoom de acessibilidade.- O arrasto é só de toque. Com mouse, arrastar dentro de um painel de texto é seleção de texto; roubá-la seria pior que não ter o atalho.
-
A trava de rolagem é a do core, e mudou de alvo. Era
overflow:hiddenno<body>, guardado por painel; agora éLIB.travarRolagem()/LIB.soltarRolagem(), que escrevem no<html>— o elemento que de fato rola —, guardam o valor anterior (não string vazia: a página consumidora pode teroverflowinline por outro motivo) e contam as travas, soltando só na última. O ganho não é de estilo: com um aviso de cookies aberto junto, a versão local destravava a página por baixo dele. Medido nos dois lados da troca, a 1440×900 e a 390×844:scrollY211 (390: 231) antes de abrir, o mesmo com o painel aberto e a roda girando 700px, e o mesmo depois de fechar;clientWidth1440 (390: 390) nos três momentos — a página não pula de largura, porque oscrollbar-gutter:stabledo core mantém a calha reservada mesmo travada. OtravaAntesdo registro de estado sumiu: a contagem é do core. -
O
--dialog-bluré declarado no::backdrop, não no.drw. Até o Chrome 121 o::backdropnão herdava custom property do<dialog>de origem — um valor posto no diálogo chegaria vazio e o desfoque cairia no default de 6px sem avisar. Declarado na própria regra do pseudo-elemento, não há herança no caminho. O--veilda primitiva ainda depende dela: em navegador anterior ao 121 o véu sumiria, e a correção, se um dia for preciso, é um fallback nocore/base.css— não aqui. -
backgroundecolordo painel resistem à primitiva de propósito. A.dialog-cascadevolvebackground:transparentecolor:inheritpara zerar a caixa do navegador; nos outros diálogos da casa isso está certo, porque o<dialog>é só o palco. Aqui ele é o painel. As duas declarações locais vencem por ordem de folha (o core carrega antes, §10 do contrato) — apagá-las deixa o painel transparente sobre o véu. - O painel encosta na coluna de rolagem, não na borda da janela. O core declara
scrollbar-gutter:stablenohtml, e o painel fica a 15px doinnerWidthno Chrome do Windows. É a mesma reserva que o resto da página respeita — não tente compensar com100vw, que é justamente o erro do §4 do manual. -
overflow:hiddenno painel, e não ovisibleda primitiva. A.dialog-cascarelaxa ooverflow:autodo agente do usuário paravisible, que basta para o diálogo não rolar. Aqui a declaração local fica porque cabeça, corpo e pé entram comfilter:blur(12px), e o desfoque de um item que ocupa a largura inteira da faixa pinta para fora dela — sobre o véu, à esquerda da borda do painel. Clipar na caixa do diálogo é o que mantém o deslize com uma borda só. -
O véu escurece 78%, e não 74%. A peça existe para manter o contexto visível atrás, então adotar o
--veilda casa foi decisão medida, não automática. Fotografada a faixa de 905×900 que fica à esquerda do painel, a 1440×900: a mediana da página atrás fica no cinza 12 nos dois graus, e o 0,1% mais claro dela cai do cinza 40 para o 36 — quatro níveis em 255. O contraste interno do que se vê atrás vai de 1,33 para 1,26, e nos dois casos está muito abaixo de qualquer limite de leitura: ali a página é presença, não texto. Como o fundo do próprio painel é o cinza 24, entre a mediana e o pico, o contexto continua se lendo atrás dele. Quatro pontos de alfa pelo fim de um número que vivia em dois lugares. -
Movimento reduzido não desliga a peça. O painel continua abrindo e fechando, porque foi pedido; some só a coreografia. As regras de desligamento repetem os dois grupos de seletor porque a regra de abertura vale (0,3,0) e uma versão curta perderia em silêncio. Achado da medição de 06/09/2026: o
::backdropé o único pedaço que continua acendendo em--drw-dursobreduce(medido:transition-duration0.44sno pseudo-elemento, contra0sno painel, nas três faixas e no deslize). Ooverlay/modal-dialogodesliga o dele. Um.drw::backdrop{transition:none}dentro do@mediafecharia a diferença — ficou fora do refactor de duplicação porque muda o que se vê, ainda que só para quem pediu menos movimento.
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| autoral | — | 2026-09-06 | Construído aqui, sem referência única. |
Origem. Componente autoral. O padrão — painel lateral de detalhe, side drawer ou off-canvas panel — aparece em três famílias de exemplo: carrinho e filtros de e-commerce, painel de referência de documentação técnica, e perfil de pessoa aberto sobre a grade em site institucional premium. Nenhuma linha veio de lugar nenhum; o que veio das famílias foi a proporção (faixa de ~500px contra tela cheia), o vocabulário de interação (cabeçalho parado, corpo rolando, arrastar para fechar no toque) e a expectativa de que o fundo continue visível.
O que a tradução para o contrato decidiu. Três coisas, e as três são o oposto do que a maioria dos exemplos faz:
<dialog>nativo comshowModal(), em vez dediv[role=dialog]comhiddene um focus trap escrito à mão. Foco preso, Esc, foco devolvido einertno resto da página vêm de graça e testados. O preço é reposicionar um elemento que nasce centrado, e isso se resolve em CSS.- Conteúdo em HTML estático, movido para dentro do painel — não clonado, não injetado por JSON, não buscado por
fetch. É o que faz a página continuar legível sem JavaScript, que era requisito e não cortesia: perfil de assessor é conteúdo editorial. - O bloco de origem escondido pelo script, na última linha — nunca pelo CSS. A diferença aparece só no dia em que o script não roda, e é a diferença entre uma página inteira e uma página pela metade.
06/09/2026 — três promoções do core adotadas no mesmo dia. Poucas horas depois de o componente entrar, .dialog-casca, LIB.travarRolagem e --veil viraram core, tirados do que quatro sobreposições escreviam igual — e este foi um dos dois que consomem os três de uma vez. Saíram do arquivo local sete declarações de CSS (padding, border e os dois max-* do diálogo; background, backdrop-filter e o par -webkit- do ::backdrop) e o estado de trava por painel do script, que virou uma chamada guardada. Nada mudou na tela: painel 520×900 no desktop e 375×844 no celular, corpo 650px de altura, desfoque do véu blur(7px), entrada em três faixas com 0s/.09s/.18s, foco, Esc, clique no fundo e arrasto idênticos. Mudou de propósito um valor só — o véu, de 74% para 78% —, medido em pixel e registrado nos Gotchas.
Normalizações aplicadas: prefixo drw- em toda classe; .btn-disc do core no lugar de um sexto botão-disco copiado; .glass/.glass-hover, .pill, .btn/.btn-primary, .sec-head, .eyebrow e .nota (as ressalvas) consumidos, não recopiados; entrada em três faixas escrita no próprio componente (o data-rv do core é IntersectionObserver e revela uma vez só, o que não serve a um painel que abre e fecha); @media(prefers-reduced-motion) colado a cada regra que ele desliga.
Divergências conhecidas vs. produção: nenhuma — o componente ainda não rodou num site. O primeiro uso real deve mexer em duas coisas: a largura (--drw-w) contra o comprimento real do texto do cliente, e a troca do monograma por retrato, que pede a autorização de imagem descrita em Compliance.
Verificação (2026-09-06, Chrome headless, bin/medir.js e roteiro próprio): console limpo em 1440×900, 390×844, --reduce e sem JavaScript; zero overflow horizontal nos quatro. Foco entra no botão de fechar ao abrir e volta ao card ao fechar (Esc, botão, clique no fundo e arrasto). Abrir o item 1, fechar e abrir o item 3 entrega o texto do item 3; zero id duplicado com o painel aberto; os quatro blocos voltam ao lugar e à ordem original. Roda do mouse com o painel aberto: scrollY 0 antes e 0 depois; ao fechar, o overflow inline volta a "" e a página não salta. No celular o painel mede 390 de 390. Arrasto horizontal de 40px devolve o painel; de 160px fecha e devolve o bloco; arrasto vertical de 200px não move o painel nem o fecha. Sob --reduce, transition-duration 0s no painel e nas três faixas, e ele continua abrindo e fechando.
Reverificação (2026-09-06, depois de adotar as três promoções do core): o mesmo roteiro rodado antes e depois da troca, em 1440×900, 390×844 e --reduce, comparando 741 valores medidos. Bateram todos, menos os esperados: a classe nova no <dialog>, max-width/max-height do diálogo passando de 100% para none sem que uma única caixa mudasse (painel 520×900, corpo 520×649,8, pé 520×143,4, idênticos ao pixel), o alvo da trava indo do <body> para o <html>, e o véu de 74% para 78%. scrollY e clientWidth iguais nos três momentos da trava, console limpo nos seis cenários, bin/conformidade.py em 0 alertas.