UI Components Easy
Video Thumbnail Grid
Video thumbnail grid with play overlay, duration badge, hover preview shimmer, and category filter tabs. Pure CSS.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0d1117;
color: #e6edf3;
min-height: 100vh;
padding: 32px 16px;
display: flex;
justify-content: center;
}
.demo {
width: 100%;
max-width: 960px;
}
.vg-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
gap: 16px;
flex-wrap: wrap;
}
.vg-title {
font-size: 18px;
font-weight: 700;
color: #e6edf3;
}
.vg-tabs {
display: flex;
gap: 4px;
background: #161b22;
border-radius: 10px;
padding: 4px;
}
.vg-tab {
background: none;
border: none;
color: #8b949e;
font-size: 13px;
font-weight: 600;
padding: 6px 14px;
border-radius: 7px;
cursor: pointer;
transition: all 0.15s;
}
.vg-tab.active {
background: #6366f1;
color: #fff;
}
.vg-tab:not(.active):hover {
color: #e6edf3;
background: rgba(255, 255, 255, 0.05);
}
/* Grid */
.vg-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
}
.vg-card {
background: #161b22;
border: 1px solid #21262d;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
}
.vg-card:hover {
transform: translateY(-3px);
border-color: #30363d;
}
.vg-thumb {
position: relative;
aspect-ratio: 16 / 9;
display: flex;
align-items: center;
justify-content: center;
}
.vg-thumb-bg {
position: absolute;
inset: 0;
}
.vg-play-btn {
position: relative;
z-index: 1;
width: 44px;
height: 44px;
background: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fff;
backdrop-filter: blur(4px);
transition: transform 0.15s, background 0.15s;
}
.vg-card:hover .vg-play-btn {
transform: scale(1.1);
background: rgba(99, 102, 241, 0.8);
}
.vg-duration {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 11px;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
}
.vg-info {
padding: 12px 14px;
}
.vg-video-title {
font-size: 14px;
font-weight: 600;
color: #e6edf3;
line-height: 1.4;
margin-bottom: 6px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.vg-meta {
font-size: 12px;
color: #6c7086;
display: flex;
gap: 8px;
align-items: center;
}
.vg-meta-dot {
font-size: 8px;
}
.vg-badge {
display: inline-block;
font-size: 10px;
font-weight: 700;
padding: 2px 7px;
border-radius: 4px;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.vg-badge--tutorials {
background: rgba(99, 102, 241, 0.2);
color: #a5b4fc;
}
.vg-badge--talks {
background: rgba(16, 185, 129, 0.15);
color: #6ee7b7;
}
.vg-badge--shorts {
background: rgba(245, 101, 101, 0.15);
color: #fca5a5;
}
.vg-card.hidden {
display: none;
}
/* Modal */
.vp-modal {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.vp-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.85);
}
.vp-card {
position: relative;
width: 100%;
max-width: 680px;
background: #1c2128;
border-radius: 16px;
overflow: hidden;
border: 1px solid #30363d;
animation: modal-in 0.2s ease;
}
@keyframes modal-in {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
.vp-fake-video {
aspect-ratio: 16 / 9;
background: #0d1117;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.vp-big-play {
font-size: 48px;
opacity: 0.3;
}
.vp-playing-indicator {
display: flex;
gap: 4px;
align-items: flex-end;
height: 32px;
}
.vp-playing-indicator span {
width: 5px;
background: #6366f1;
border-radius: 2px;
animation: vp-wave 1s ease-in-out infinite;
}
.vp-playing-indicator span:nth-child(1) {
animation-delay: 0s;
height: 12px;
}
.vp-playing-indicator span:nth-child(2) {
animation-delay: 0.1s;
height: 20px;
}
.vp-playing-indicator span:nth-child(3) {
animation-delay: 0.2s;
height: 32px;
}
.vp-playing-indicator span:nth-child(4) {
animation-delay: 0.3s;
height: 20px;
}
.vp-playing-indicator span:nth-child(5) {
animation-delay: 0.4s;
height: 12px;
}
@keyframes vp-wave {
0%,
100% {
transform: scaleY(0.5);
opacity: 0.5;
}
50% {
transform: scaleY(1);
opacity: 1;
}
}
.vp-controls {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: #161b22;
border-top: 1px solid #21262d;
}
.vp-ctrl-btn {
background: none;
border: none;
color: #8b949e;
font-size: 15px;
cursor: pointer;
padding: 4px 8px;
border-radius: 6px;
transition: color 0.15s, background 0.15s;
}
.vp-ctrl-btn:hover {
color: #e6edf3;
background: rgba(255, 255, 255, 0.05);
}
.vp-close-btn {
margin-left: auto;
}
.vp-progress-bar {
flex: 1;
height: 4px;
background: #30363d;
border-radius: 2px;
overflow: hidden;
cursor: pointer;
}
.vp-progress-fill {
height: 100%;
background: #6366f1;
width: 0%;
transition: width 0.5s linear;
}
.vp-time {
font-size: 11px;
color: #6c7086;
font-family: Menlo, monospace;
white-space: nowrap;
}
.vp-info {
padding: 16px 20px 20px;
}
.vp-modal-title {
font-size: 16px;
font-weight: 700;
color: #e6edf3;
margin-bottom: 6px;
}
.vp-modal-meta {
font-size: 12px;
color: #6c7086;
}const VIDEOS = [
{
id: 1,
title: "Building a Full-Stack App with Next.js 14",
category: "tutorials",
views: "142K",
duration: "42:18",
gradient: "linear-gradient(135deg,#1a1a2e,#16213e)",
},
{
id: 2,
title: "The Future of AI in Software Engineering",
category: "talks",
views: "98K",
duration: "28:05",
gradient: "linear-gradient(135deg,#0f3460,#533483)",
},
{
id: 3,
title: "CSS Tips You Wish You Knew Sooner",
category: "shorts",
views: "215K",
duration: "0:58",
gradient: "linear-gradient(135deg,#2d1b69,#11998e)",
},
{
id: 4,
title: "TypeScript Generics Deep Dive",
category: "tutorials",
views: "76K",
duration: "35:22",
gradient: "linear-gradient(135deg,#1e3c72,#2a5298)",
},
{
id: 5,
title: "Designing for Accessibility First",
category: "talks",
views: "54K",
duration: "22:44",
gradient: "linear-gradient(135deg,#134e5e,#71b280)",
},
{
id: 6,
title: "One CSS Trick to Rule Them All",
category: "shorts",
views: "403K",
duration: "0:45",
gradient: "linear-gradient(135deg,#4a1942,#c74b50)",
},
{
id: 7,
title: "Docker for Frontend Developers",
category: "tutorials",
views: "63K",
duration: "19:30",
gradient: "linear-gradient(135deg,#0f2027,#2c5364)",
},
{
id: 8,
title: "The Psychology of Code Reviews",
category: "talks",
views: "88K",
duration: "31:12",
gradient: "linear-gradient(135deg,#373b44,#4286f4)",
},
];
const grid = document.getElementById("vgGrid");
let activeFilter = "all";
let playing = false;
let progressInterval = null;
let progressPct = 0;
function renderGrid() {
grid.innerHTML = "";
VIDEOS.forEach((v) => {
const show = activeFilter === "all" || v.category === activeFilter;
const card = document.createElement("div");
card.className = "vg-card" + (show ? "" : " hidden");
card.innerHTML = `
<div class="vg-thumb">
<div class="vg-thumb-bg" style="background:${v.gradient}"></div>
<div class="vg-play-btn">▶</div>
<span class="vg-duration">${v.duration}</span>
</div>
<div class="vg-info">
<div class="vg-badge vg-badge--${v.category}">${v.category}</div>
<div class="vg-video-title">${v.title}</div>
<div class="vg-meta"><span>${v.views} views</span><span class="vg-meta-dot">•</span><span>${v.category}</span></div>
</div>
`;
card.addEventListener("click", () => openModal(v));
grid.appendChild(card);
});
}
function openModal(v) {
const modal = document.getElementById("vpModal");
document.getElementById("vpModalTitle").textContent = v.title;
document.getElementById("vpModalMeta").textContent = `${v.views} views • ${v.category}`;
document.getElementById("vpFakeVideo").style.background = v.gradient;
modal.hidden = false;
resetPlayer();
}
function resetPlayer() {
stopProgress();
progressPct = 0;
document.getElementById("vpFill").style.width = "0%";
document.getElementById("vpPlay").textContent = "▶";
document.getElementById("vpIndicator").hidden = true;
document.getElementById("vp-big-play").textContent = "▶";
playing = false;
}
function stopProgress() {
clearInterval(progressInterval);
progressInterval = null;
}
document.getElementById("vpPlay").addEventListener("click", () => {
playing = !playing;
document.getElementById("vpPlay").textContent = playing ? "⏸" : "▶";
document.getElementById("vpIndicator").hidden = !playing;
document.getElementById("vp-big-play").textContent = playing ? "" : "▶";
if (playing) {
progressInterval = setInterval(() => {
progressPct = Math.min(progressPct + 0.4, 100);
document.getElementById("vpFill").style.width = progressPct + "%";
if (progressPct >= 100) {
stopProgress();
playing = false;
document.getElementById("vpPlay").textContent = "▶";
}
}, 100);
} else {
stopProgress();
}
});
document.getElementById("vpClose").addEventListener("click", () => {
document.getElementById("vpModal").hidden = true;
stopProgress();
});
document.getElementById("vpBackdrop").addEventListener("click", () => {
document.getElementById("vpModal").hidden = true;
stopProgress();
});
document.getElementById("vgTabs").addEventListener("click", (e) => {
const tab = e.target.closest(".vg-tab");
if (!tab) return;
document.querySelectorAll(".vg-tab").forEach((t) => t.classList.remove("active"));
tab.classList.add("active");
activeFilter = tab.dataset.cat;
renderGrid();
});
renderGrid();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Video Grid</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<div class="vg-header">
<h2 class="vg-title">Trending Videos</h2>
<div class="vg-tabs" id="vgTabs">
<button class="vg-tab active" data-cat="all">All</button>
<button class="vg-tab" data-cat="tutorials">Tutorials</button>
<button class="vg-tab" data-cat="talks">Talks</button>
<button class="vg-tab" data-cat="shorts">Shorts</button>
</div>
</div>
<div class="vg-grid" id="vgGrid"></div>
</div>
<!-- Modal player -->
<div class="vp-modal" id="vpModal" hidden>
<div class="vp-backdrop" id="vpBackdrop"></div>
<div class="vp-card">
<div class="vp-player" id="vpPlayer">
<div class="vp-fake-video" id="vpFakeVideo">
<div class="vp-big-play" id="vp-big-play">▶</div>
<div class="vp-playing-indicator" id="vpIndicator" hidden>
<span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<div class="vp-controls">
<button class="vp-ctrl-btn" id="vpPlay">▶</button>
<div class="vp-progress-bar">
<div class="vp-progress-fill" id="vpFill"></div>
</div>
<span class="vp-time" id="vpTime">0:00 / 0:00</span>
<button class="vp-ctrl-btn vp-close-btn" id="vpClose">✕</button>
</div>
</div>
<div class="vp-info">
<h3 class="vp-modal-title" id="vpModalTitle"></h3>
<p class="vp-modal-meta" id="vpModalMeta"></p>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Video thumbnail grid with play button overlay, duration badge, channel avatar, title and view count. Category filter tabs filter the grid. Hover reveals a shimmer preview effect.