← Biblioteca componentes/overlay/card-expansivel-fullscreen/ficha.md Abrir previewmarkupestiloscript
overlay/card-expansivel-fullscreen

Card que expande para tela cheia

🔵 experimental complexidade alta prefixo exp- overlay modal dialogo cases portfolio trilho flip acessibilidade

Um trilho horizontal de cards retrato: foto, categoria em cor de marca e título dentro da imagem. No clique, o card não abre um modal centralizado — ele cresce do lugar exato onde está até cobrir a tela, com o raio da quina indo a zero, e só quando termina de crescer o conteúdo entra: foto cheia de um lado, categoria, título, texto, tags e CTA do outro.

O que resolve editorialmente: uma coleção de casos em que cada item merece meia página de leitura, mas nenhum merece uma página inteira. A grade comum obriga a escolher entre mostrar tudo (e virar parede de texto) ou mostrar só o título (e não convencer ninguém). Aqui o card mostra o mínimo, o overlay mostra o suficiente, e a transição entre os dois deixa claro que é o mesmo item — que é a coisa que um modal centralizado nunca consegue dizer.

É o primeiro componente da categoria sobreposições.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Olho (.eyebrow) 1 a 2 palavras. O tipo de coleção: "Trabalhos", "Casos", "Portfólio".
Título (.sec-title) 1 linha, uma palavra em <em>. Nomeia o conjunto, não convida ao clique: "O trabalho fala por si".
Apoio (.sec-lead) 1 parágrafo, 20 a 35 palavras. Diz o critério de entrada da coleção — por que estes casos e não outros.
data-cat 1 a 2 palavras, a mesma taxonomia em todos os cards. Categorias soltas ("Branding", "Projeto especial", "Outros") denunciam que não existe critério.
data-title 4 a 8 palavras. Nomeia o que aconteceu, não o cliente: "A conversa que a família adiava" vale mais que o nome da empresa — e não expõe cliente nenhum.
data-desc 35 a 55 palavras, em um parágrafo só. Passado, sem jargão interno. Todos os cards com comprimento parecido: a coluna centraliza na vertical, e um texto muito mais curto que os vizinhos desequilibra a troca de um card para o outro.
data-tags 3 a 4 rótulos, 1 a 2 palavras cada, separados por vírgula. São frentes de trabalho, não adjetivos.
alt da foto Obrigatório e descritivo. É o único conteúdo que chega a quem não vê a imagem — e ele reaparece no overlay.
CTA Um só, o mesmo para todos os cards, no markup. Só use data-href por card quando cada caso tiver mesmo um destino diferente.

Use de 4 a 10 cards. Com 3 o trilho não transborda em desktop, a barra de progresso some sozinha e o gesto de rolar lateralmente nunca acontece — a seção fica prometendo um movimento que não existe. Acima de 10, ninguém chega ao fim.


Especificação das imagens

Proporção retrato, 3:4 — a caixa do card é aspect-ratio:3/4
Mínimo 900×1200. No overlay a foto ocupa metade da tela em altura cheia
Recorte object-fit:cover nos dois lugares. O assunto tem de sobreviver a corte lateral no card e a corte vertical no overlay
Peso com loading="lazy", só as visíveis carregam de início; ainda assim, abaixo de 250 KB cada
Tom fotos de contraste médio para escuro. O card recebe grayscale(.25) em repouso e volta ao normal no hover — foto já dessaturada não tem para onde ir

data-img existe para apontar uma versão maior da mesma foto: o card pode carregar um recorte leve e o overlay a imagem cheia. Quando as duas são a mesma, pode omitir — o script cai na foto do card. O script pré-carrega a versão grande no primeiro hover ou foco do card, então ela chega antes da animação terminar.


Como usar

<link rel="stylesheet" href="componentes/overlay/card-expansivel-fullscreen/estilo.css">
<script src="componentes/overlay/card-expansivel-fullscreen/script.js"></script>

Cole markup.html, estilo.css e script.js. Depende do core (tokens, base, motion) e de mais nada.

Card novo é só mais um <li>. Todo o conteúdo do overlay mora nos data-* do card; não existe um segundo bloco de HTML por caso, e não há nada para manter em sincronia.

<li class="exp-item">
  <button class="exp-card" type="button" data-exp-card aria-haspopup="dialog"
          data-img="img/caso-1-grande.webp"
          data-cat="Consultoria"
          data-title="Título do caso"
          data-desc="Parágrafo de 35 a 55 palavras."
          data-tags="Uma,Duas,Três">
    <img class="exp-card-img" src="img/caso-1.webp" alt="Descrição da foto" loading="lazy" decoding="async">
    ...
  </button>
</li>

Ganchos

Gancho Papel
[data-exp] a raiz. O script aceita vários trilhos na mesma página, cada um com o seu overlay
[data-exp-track] o trilho que rola. É aqui que vai o data-rv, nunca no .exp
[data-exp-card] o card. Um <button> de verdade, com aria-haspopup="dialog"
[data-exp-overlay] o diálogo. role="dialog", aria-modal="true", hidden no repouso
[data-exp-close], [data-exp-rail] botão de fechar e barra de progresso do trilho
[data-exp-img|cat|title|desc|tags|cta] os destinos dentro do overlay. O script preenche
.exp-overlay.is-shown conteúdo visível — posto depois que a caixa terminou de crescer
.exp.is-scrollable o trilho transborda. Só aí a barra aparece
.exp-overlay.is-measuring janela de medição, com a transição desligada. Estado interno do script
--exp-tx/ty/sx/sy/r o canal do FLIP: o script escreve, o CSS monta transform e border-radius
--exp-thumb, --exp-shift o canal da barra, em frações de 0 a 1
--exp-dur, --exp-fade duração da caixa e do conteúdo
--exp-card-w, --exp-gap, --exp-z largura do card, vão do trilho e camada do overlay

Atributos do card

Atributo Obrigatório O quê
data-cat sim categoria; vira o olho do overlay
data-title sim título; vira o rótulo acessível do diálogo
data-desc sim o parágrafo
data-tags não lista separada por vírgula; vira .pill do core. Ausente, o bloco some
data-img não versão maior da foto. Ausente, usa a foto do card
data-alt não alt da versão maior. Ausente, usa o alt da foto do card
data-href não destino do CTA só para este card. Ausente, usa o href do markup

Variações

Variação Como
Quantidade de cards 4 a 10. Abaixo de 4 a barra some e o trilho não rola
Card sem overlay tire o data-exp-card e troque o <button> por <a href>. O card vira link para a página do caso
Largura do card --exp-card-w no .exp
Ritmo --exp-dur (.52s) e --exp-fade (.34s) no .exp
Camada --exp-z (500). Suba se o menu fixo do projeto passar disso

Gotchas


Histórico

Projeto Onde Data Diferenças
Gabriel Rocha Studio gabriel-rocha-studio-2.html, #cases 2026-08 Origem. Site autoral do próprio estúdio, rodou uma vez. GSAP com timeline, <div> clicável, top/left/width/height animados, innerHTML nas tags, sem role="dialog", sem media query própria.

Status experimental: rodou uma vez, num site só, e foi bastante reescrito na travessia. Espere ajuste no segundo uso.

O que mudou na travessia

No original Aqui Por quê
GSAP timeline com power3.inOut FLIP em transform, encadeado por transitionend O contrato só admite GSAP para timeline encadeada de verdade ou ScrollTrigger com scrub. Duas etapas em sequência resolvem-se com dois transitionend
Animava top, left, width, height translate() + scale() com transform-origin:0 0 Aquelas quatro propriedades disparam layout a cada quadro; transform é composto na GPU. A expansão passa a ser barata mesmo em telefone
borderRadius:16 cru --radius-media lido do card, dividido pela escala Cor, forma e raio vêm do tema. E sem a divisão o raio sairia esmagado junto com a caixa
<div class="exp-card"> com listener de clique <button type="button"> com aria-haspopup="dialog" Foco, Enter e Espaço de graça; e o card entra na ordem de tabulação sem tabindex inventado
Sem role, sem aria-modal, sem foco preso, sem devolução de foco role="dialog", aria-modal="true", aria-labelledby com id gerado, Tab preso e foco de volta ao card Solução do navegacao/drawer-fullscreen, com o foco preso que faltava lá — aqui é modal de verdade
document.body.style.overflow='hidden' e depois '' Guarda o valor anterior e repõe a largura da barra de rolagem medindo antes/depois O original apagava qualquer overflow que a página já tivesse e deixava a página saltar para a direita na abertura
expTags.innerHTML = ...split(',').map('<span…>') createElement + textContent Montar HTML concatenando um atributo injeta marcação assim que alguém escrever < no CMS
.exp-content{grid-template-columns:1fr 1fr} sem media query empilha abaixo de 900px; foto some em paisagem baixa No original, telefone recebia duas colunas de 180px: foto ilegível de um lado, parágrafo de 45 palavras do outro
Fechar clicando fora comparava só com o overlay compara com overlay, grade e coluna de texto A grade cobre o overlay inteiro, então o alvo era sempre um filho: o clique-fora do original nunca fechava nada
stroke="white" e stroke="#fff" nos SVG stroke="currentColor" Ícone tem de virar junto com o tema
Barra de rolagem escondida, sem nada no lugar escondida + .exp-rail com progresso A informação de "ainda tem mais para o lado" tinha sumido junto com a barra
.exp-card-overlay (gradiente) + .exp-card-info (texto) um bloco só, com gradiente, backdrop-filter e máscara O véu do contrato §6 pede máscara em gradiente; e duas caixas empilhadas para o mesmo efeito viravam duas camadas de composição por card
Cores cruas (rgba(6,7,10,.88), #3a3a36, rgba(255,255,255,.1)) --dark-rgb, --rule-2, primitiva .glass O mesmo componente tem de rodar em qualquer tema
.exp-tag com borda, raio e tracking próprios primitiva .pill do core Era a terceira cópia da mesma pílula na biblioteca (contrato §6)
.cases-head com h2 e .eyebrow próprios .sec-head + h2.sec-title + .sec-lead do core Cabeçalho de dobra já é primitiva
Sem tratamento de movimento reduzido FLIP desligado, durações a .01ms, hover sem scale Exigência do contrato §5
Fotos por caminho de imgs/ do site placeholders locais do showroom Conteúdo de cliente não entra na biblioteca
Nomes de projeto inventados no demo slots descritivos e genéricos Demo da biblioteca não nomeia marca nem pessoa