card-image — card com imagem estática

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.

CÓDIGO COMPLETO

index.html

<!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>

style.css

/* 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%);
}

script.js

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;
        });
    }
});
Como usar — pra quem vai aplicar numa página
0:00 / 0:00

Quando usar

Usar quando:

  • Uma imagem estática representa bem o conteúdo
  • Ainda não existe vídeo gerado
  • A mídia é simbólica/decorativa, não demonstrativa
  • A página precisa de cards de navegação visual

Não usar quando:

  • Já existe um .mp4 mostrando interação ou movimento → use card-video

HTML mínimo

<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".

Atributos

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.

Variação sem data-href

Se 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.

Como funciona por dentro — pra quem quer entender a mecânica
0:00 / 0:00

Anatomia

.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

Hover 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.

Card inteiro clicável

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.

Menu de info abre painel

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');
});

Blur sem movimento ao abrir o painel

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.

Decisões de design

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.

Comportamento no mobile

Touch devices não disparam :hover:

  • O efeito visual de hover (mudança de cor da .card-tag) não aparece
  • Click no card vai direto pro destino via data-href
  • O painel .card-info abre via tap no menu de 3 bolinhas — handler usa e.stopPropagation() pra não disparar o navigate

Diferente 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".