Variante do componente card com vídeo em loop autoplay como mídia de fundo, botão pause/play que aparece no hover, e painel de info contendo um texto descritivo (.card-resumo). Para variante com lista de sub-links, veja card-video-link.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>card-video — variante texto</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>Demo</strong>
<p>Vídeo em loop autoplay no primeiro hover. Botão pause/play aparece no canto superior esquerdo ao passar o mouse.</p>
</div>
</div>
<div class="card-img">
<button class="card-pause btnPauseCard" aria-label="Pausar/tocar vídeo">
<svg class="ic-pause" viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>
<svg class="ic-play" viewBox="0 0 24 24"><path d="M7 4v16l13-8z"/></svg>
</button>
<video src="video.mp4#t=0.1" muted loop playsinline preload="auto" data-name="DEMO"></video>
</div>
<div class="card-bar">
<a href="https://example.com" class="card-tag" style="--tag-clr:#10b981;">Demo</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;
background: #1a1a1a;
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;
}
/* --- Mídia --- */
.card-img {
position: absolute;
inset: 0;
overflow: hidden;
bottom: 56px;
}
.card-img video {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top;
transition: filter 0.5s ease;
}
/* --- 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;
}
/* --- Botão pause/play (canto superior esquerdo) --- */
.card-pause {
position: absolute;
left: 8px;
top: 8px;
z-index: 5;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
background: rgba(255,255,255,0.25);
border: none;
border-radius: 6px;
padding: 4px;
box-sizing: content-box;
display: flex;
align-items: center;
justify-content: center;
}
.card:hover .card-pause {
opacity: 1;
}
.card:has(.card-info.aberto) .card-pause {
opacity: 0;
pointer-events: none;
}
.card-pause svg {
width: 24px;
height: 24px;
fill: #fff;
filter: drop-shadow(0 0 3px rgba(0,0,0,0.5));
display: block;
}
.card-pause .ic-pause { display: none; }
.card-pause.playing .ic-pause { display: block; }
.card-pause.playing .ic-play { display: none; }
/* --- 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 mídia quando info aberto */
.card-info.aberto ~ .card-img video {
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;
});
}
var video = card.querySelector('.card-img video');
if (video) {
card.addEventListener('mouseenter', function() {
video.play().catch(function() {});
}, { once: true });
var pauseBtn = card.querySelector('.btnPauseCard');
if (pauseBtn) {
pauseBtn.addEventListener('click', function(e) {
e.stopPropagation();
if (video.paused) video.play().catch(function() {});
else video.pause();
});
video.addEventListener('play', function() { pauseBtn.classList.add('playing'); });
video.addEventListener('pause', function() { pauseBtn.classList.remove('playing'); });
}
}
});
Usar quando:
.mp4 já gerado e quer mostrar uma descrição em texto livreNão usar quando:
card-imagecard-video-link<div class="card" data-href="...">
<div class="card-menu btnMenuCard">
<svg viewBox="0 0 350 350">...3 bolinhas...</svg>
</div>
<div class="card-info">
<div class="card-resumo">
<strong>Demo</strong>
<p>Texto descritivo do card.</p>
</div>
</div>
<div class="card-img">
<button class="card-pause btnPauseCard" aria-label="Pausar/tocar vídeo">
<svg class="ic-pause" viewBox="0 0 24 24">...</svg>
<svg class="ic-play" viewBox="0 0 24 24">...</svg>
</button>
<video src="video.mp4#t=0.1" muted loop playsinline preload="auto" data-name="DEMO"></video>
</div>
<div class="card-bar">
<a href="..." class="card-tag" style="--tag-clr:#10b981;">Demo</a>
</div>
</div>
<video>| Atributo | Por que |
|---|---|
src="...#t=0.1" |
O #t=0.1 força o browser a renderizar o frame em 0.1s em vez de tela preta. |
muted |
Browsers bloqueiam autoplay se o vídeo tiver áudio. |
loop |
Vídeo recomeça automaticamente; tem que ser curto (5–15s). |
playsinline |
iOS Safari não toma fullscreen ao tentar tocar. |
preload="auto" |
Browser baixa o vídeo logo no load (imagem pronta antes do hover). |
data-name |
Nome lógico do vídeo/card. Não controla o texto do botão. |
.card
├── .card-menu .btnMenuCard hamburger top-right (abre painel info)
├── .card-info painel deslizante (.aberto = visível)
│ └── .card-resumo strong + p de descrição
├── .card-img vídeo cobrindo o card
│ ├── .card-pause .btnPauseCard botão pause/play top-left
│ │ ├── svg.ic-pause
│ │ └── svg.ic-play
│ └── <video muted loop playsinline preload="auto">
└── .card-bar
└── a.card-tag nome fixo do card
Vídeo começa pausado. O { once: true } garante que o autoplay só dispara uma vez — depois disso o controle é do botão pause/play:
card.addEventListener('mouseenter', function() {
video.play().catch(function() {});
}, { once: true });
O .catch(noop) evita warning no console — browsers retornam Promise rejeitada se autoplay for bloqueado pela política do user agent.
pauseBtn.addEventListener('click', function(e) {
e.stopPropagation();
if (video.paused) video.play().catch(function() {});
else video.pause();
});
video.addEventListener('play', function() { pauseBtn.classList.add('playing'); });
video.addEventListener('pause', function() { pauseBtn.classList.remove('playing'); });
O e.stopPropagation() impede que o click propague pro card (que pode ter data-href) — sem isso, tentar pausar acabava redirecionando.
.playing.card-pause .ic-pause { display: none; }
.card-pause.playing .ic-pause { display: block; }
.card-pause.playing .ic-play { display: none; }
O CSS controla qual SVG mostrar baseado em .playing. O JS só adiciona/remove a classe nos eventos play/pause do video. Mais robusto: se o vídeo termina o loop em momento estranho, o estado visual continua sincronizado.
.card:has(.card-info.aberto) .card-pause {
opacity: 0;
pointer-events: none;
}
Sem isso, o botão ficaria flutuando sobre o painel aberto e podendo receber click acidental. Quando o painel fecha, a regra :hover reassume e o botão reaparece naturalmente.
Por que vídeo começa pausado?
Autoplay imediato em todos os cards consome CPU/bateria sem benefício se o usuário não está interessado. O modelo "primeiro hover dá play" é responsivo à intenção: cursor sobre o card já sinaliza interesse.
Por que { once: true } no listener do mouseenter?
Garante que o autoplay só dispara uma vez. Depois disso o controle é manual (botão).
Por que botão pause oculto até hover?
opacity: 0 por padrão, 1 no :hover do card. Mantém o card visualmente limpo e o botão só aparece quando relevante (mouse já está perto).
Por que troca de ícone via classe .playing em vez de JS direto?
CSS controla qual SVG mostrar baseado em .playing. Mais robusto: se o vídeo termina o loop em momento estranho, o estado visual continua sincronizado com o estado real do <video>.
Por que #t=0.1 no src?
Força renderização do primeiro frame antes do user dar hover. Sem isso, o card mostra preto até o vídeo carregar.
Por que mídia só com blur ao abrir o painel?
A versão antiga combinava blur com scale(1.1) pra dar profundidade, mas o movimento da mídia competia com o slide do painel e poluía a leitura. Agora o efeito é puramente óptico: o que estava nítido fica desfocado, ponto.
opacity: 0 e só vira 1 no :hover do card. Em mobile fica permanentemente invisível.mouseenter, que touch devices não disparam. Em mobile o vídeo fica no frame inicial (#t=0.1) e não toca.data-href..card-tag no hover não aparece.Limitação conhecida: se quiser que o vídeo toque no mobile, precisa de outro evento (ex: IntersectionObserver quando o card entra na viewport).