Card retangular com imagem estática como mídia de fundo, painel de info que desliza da direita ao clicar no menu de três bolinhas, e barra inferior com tag/botão. Variante irmã: card-video — pra mídia em movimento.
Veja o componente em ação na própria home de componentes — cada card listado lá usa este padrão.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>card-image — exemplo individual</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="demo-stage">
<div class="card" data-href="https://example.com">
<div class="card-menu btnMenuCard">
<svg viewBox="0 0 350 350"><circle cx="39" cy="55" r="39"/><circle cx="39" cy="175" r="39"/><circle cx="39" cy="295" r="39"/><path d="M345,21H114c-3,0-5,2-5,5v58c0,3,2,5,5,5h231c3,0,5-2,5-5V26c0-3-2-5-5-5z"/><path d="M345,141H114c-3,0-5,2-5,5v58c0,3,2,5,5,5h231c3,0,5-2,5-5v-58c0-3-2-5-5-5z"/><path d="M345,261H114c-3,0-5,2-5,5v58c0,3,2,5,5,5h231c3,0,5-2,5-5v-58c0-3-2-5-5-5z"/></svg>
</div>
<div class="card-info">
<div class="card-resumo">
<strong>Componentes</strong>
<p>Descrição curta do que esse card representa. Este painel desliza da direita ao clicar no menu de três bolinhas.</p>
</div>
</div>
<div class="card-img">
<img src="img.png" alt="Exemplo">
</div>
<div class="card-bar">
<a href="https://example.com" class="card-tag" style="--tag-clr:#8b5cf6;">Componentes</a>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
/* Stage só pra visualização standalone */
body {
margin: 0;
min-height: 100vh;
background: #f5f5f7;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
align-items: center;
justify-content: center;
}
.demo-stage {
width: 100%;
max-width: 320px;
padding: 40px;
}
/* --- Card --- */
.card {
position: relative;
width: 100%;
aspect-ratio: 3.5 / 4.5;
border-radius: 8px;
overflow: hidden;
cursor: default;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
transition: transform 0.4s ease, box-shadow 0.4s ease;
}
.card:hover {
transform: scale(1.06);
box-shadow: 0 12px 32px rgba(0,0,0,0.25);
z-index: 1;
}
/* --- Imagem --- */
.card-img {
position: absolute;
inset: 0;
overflow: hidden;
}
.card-img:has(img) {
inset: auto;
top: 0;
left: 0;
right: 0;
bottom: 56px;
}
.card-img img {
width: 130%;
height: auto;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
transition: top 0.6s ease, transform 0.6s ease, filter 0.5s ease;
}
.card:hover .card-img img {
top: 100%;
transform: translateX(-50%) translateY(-100%);
}
/* --- Barra inferior --- */
.card-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 56px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
box-shadow: 0 -3px 6px rgba(0,0,0,0.15);
z-index: 2;
}
.card-tag {
flex: 1;
text-align: center;
font-size: 0.85em;
font-weight: 700;
color: var(--tag-clr, #555);
border: 2px solid var(--tag-clr, #555);
border-radius: 6px;
padding: 8px 10px;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
text-transform: uppercase;
transition: background 0.3s, color 0.3s;
}
.card:hover .card-tag {
background: var(--tag-clr, #555);
color: #fff;
}
/* --- Botão menu (3 bolinhas) --- */
.card-menu {
position: absolute;
right: 8px;
top: 8px;
z-index: 5;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
overflow: visible;
}
.card:hover .card-menu {
opacity: 1;
}
.card-menu svg {
width: 32px;
height: 32px;
fill: #fff;
filter: drop-shadow(0 0 3px rgba(0,0,0,0.5));
background: rgba(255,255,255,0.25);
border-radius: 6px;
padding: 4px;
box-sizing: content-box;
transition: opacity 0.2s;
}
.card-menu.aberto svg {
opacity: 0;
}
.card-menu::after {
content: '\00d7';
position: absolute;
top: -16px;
right: -10px;
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
font-weight: 700;
color: #fff;
text-shadow: 0 0 4px rgba(0,0,0,0.5);
background: rgba(0,0,0,0.35);
border-radius: 10px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
cursor: pointer;
}
.card-menu.aberto::after {
opacity: 1;
}
/* --- Painel de info (slide da direita) --- */
.card-info {
position: absolute;
top: 0;
right: 0;
bottom: 56px;
overflow: hidden;
width: 100%;
padding: 0;
z-index: 4;
display: flex;
flex-direction: column;
justify-content: center;
transition: transform 0.4s ease, opacity 0.4s ease;
transform: translateX(100%);
opacity: 0;
pointer-events: none;
}
.card-info::before {
content: "";
position: absolute;
inset: 0;
background: rgba(0,0,0,0.35);
backdrop-filter: blur(4px);
opacity: 0;
transition: opacity 2s ease;
z-index: -1;
}
.card-info.aberto {
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
.card-info.aberto::before {
opacity: 1;
}
.card-resumo {
position: absolute;
top: 10px;
left: 10px;
right: 10px;
bottom: 10px;
font-size: 0.85em;
line-height: 1.6;
color: #1a1a1a;
text-align: left;
padding: 6px 8px;
overflow-y: auto;
background: rgba(255,255,255,0.4);
border-radius: 10px;
border: 1px solid rgba(0,0,0,0.08);
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.card-resumo strong {
display: block;
margin-bottom: 10px;
}
.card-resumo p {
margin: 6px 0 0;
}
/* Blur na imagem quando info aberto */
.card-info.aberto ~ .card-img img {
filter: blur(6px) grayscale(20%);
}
document.querySelectorAll('.card').forEach(function(card) {
var btn = card.querySelector('.btnMenuCard');
var info = card.querySelector('.card-info');
if (btn && info) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
info.classList.toggle('aberto');
btn.classList.toggle('aberto');
});
}
var cardHref = card.getAttribute('data-href');
if (cardHref) {
card.addEventListener('click', function(e) {
if (e.target.closest('a[href]') || e.target.closest('.btnMenuCard')) return;
window.location.href = cardHref;
});
}
});
Usar quando:
Não usar quando:
.mp4 mostrando interação ou movimento → use card-video<div class="card" data-href="pages/foo/foo.html">
<div class="card-menu btnMenuCard">
<svg viewBox="0 0 350 350">...3 bolinhas...</svg>
</div>
<div class="card-info">
<div class="card-resumo">
<p>Descrição curta do que esse card representa.</p>
</div>
</div>
<div class="card-img">
<img src="img/componentes.png" alt="Componentes">
</div>
<div class="card-bar">
<a href="pages/foo/foo.html" class="card-tag" style="--tag-clr:#8b5cf6;">Componentes</a>
</div>
</div>
O texto do <a class="card-tag"> é sempre o nome fixo do card. No hover, muda apenas cor/fundo pelo CSS — não troca pra "Ver docs".
| Elemento | Atributo | Uso |
|---|---|---|
.card |
data-href |
Faz o card inteiro navegar ao clicar fora do menu e fora do <a>. |
<img> |
src / alt |
Caminho relativo da imagem e texto acessível. |
.card-tag |
href |
Destino do link. |
.card-tag |
style="--tag-clr:#8b5cf6;" |
Cor do texto/borda e preenchimento no hover. |
.card-tag |
Texto interno | Nome fixo do card. Permanece igual em estado normal e hover. |
data-hrefSe quiser que apenas o botão/tag seja clicável, omita o data-href no wrapper. Uso raro — o padrão do projeto é card inteiro clicável.
.card
├── .card-menu .btnMenuCard menu de três bolinhas
├── .card-info painel deslizante de resumo
│ └── .card-resumo
├── .card-img
│ └── img
└── .card-bar
└── a.card-tag nome fixo do card
O card escala 1.06x e ganha sombra mais profunda. A imagem desliza de cima pra baixo (pan vertical) — o top: 100% + translateY(-100%) revela a parte de baixo da imagem que estava escondida.
O layout.js usa data-href pra transformar o card todo em área clicável:
var cardHref = card.getAttribute('data-href');
if (cardHref) {
card.addEventListener('click', function(e) {
if (e.target.closest('a[href]') || e.target.closest('.btnMenuCard')) return;
window.location.href = cardHref;
});
}
O guard e.target.closest() evita disparar quando o usuário clica no menu (3 bolinhas) ou no próprio <a> da tag.
Click no menu de três bolinhas alterna a classe .aberto no .card-info. e.stopPropagation() impede que o clique propague pro card e dispare o navigate:
btn.addEventListener('click', function(e) {
e.stopPropagation();
info.classList.toggle('aberto');
btn.classList.toggle('aberto');
});
Quando o painel está com .aberto, a imagem recebe filter: blur(6px) grayscale(20%). Não há mais transform: scale() — o efeito é puramente óptico:
.card-info.aberto ~ .card-img img {
filter: blur(6px) grayscale(20%);
}
.card-img img {
transition: top 0.6s ease, transform 0.6s ease, filter 0.5s ease;
}
A transition inclui filter pra que o blur entre/saia em fade ao abrir/fechar.
Por que tag fixa em vez de "Ver docs"?
O texto do card precisa ficar estável. Trocar para "Ver docs" no hover adiciona movimento sem acrescentar informação, além de criar diferença entre estado parado e estado hover.
Por que blur sem movimento ao abrir o painel?
A versão antiga combinava filter: blur() com transform: scale(1.1) na mídia. O scale junto com o slide do painel criava dois movimentos competindo na mesma área. A regra atual mantém só o filter, com transition em fade. A mensagem visual é clara: "isso aqui ficou em segundo plano enquanto você lê o painel".
Por que alt separado do texto do botão?
O alt descreve a imagem pra acessibilidade. O texto do .card-tag é o rótulo visual. Normalmente os dois podem ser iguais, mas um não deve controlar o outro via JavaScript.
Por que sem botão pause?
Imagem estática não tem nada pra pausar. O botão pause/play pertence apenas ao card-video.
Touch devices não disparam :hover:
.card-tag) não aparecedata-href.card-info abre via tap no menu de 3 bolinhas — handler usa e.stopPropagation() pra não disparar o navigateDiferente do codex-wrapper (carrossel), o .card não usa tap-to-reveal. O fluxo é direto: tap no card = abre. Tap no menu = abre painel. Sem estado intermediário "revelado".