Auto-advancing slides
Slides that cross-fade on a timer — achievable with pure CSS by stacking them and staggering a fade animation. (It's running below.)
HTML
<div class="slideshow"> <div class="slide">Slide 1</div> <div class="slide">Slide 2</div> <div class="slide">Slide 3</div> </div>
CSS
.slideshow { position: relative; height: 150px; border-radius: 10px; overflow: hidden; }
.slide { position: absolute; inset: 0; display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 1.3rem; opacity: 0; animation: fade 9s infinite; }
.slide:nth-child(1) { background: #38bdf8; animation-delay: 0s; }
.slide:nth-child(2) { background: #9b7bff; animation-delay: 3s; }
.slide:nth-child(3) { background: #22c55e; animation-delay: 6s; }
@keyframes fade { 0%{opacity:0} 4%,33%{opacity:1} 37%,100%{opacity:0} }
JavaScript — optional prev/next + dot controls
// Drop this in to replace the pure-CSS timer with interactive controls.
// Remove the CSS @keyframes animation and use an .active class instead:
// .slide { opacity: 0; transition: opacity .5s; }
// .slide.active { opacity: 1; }
const slides = document.querySelectorAll('.slideshow .slide');
let current = 0;
function show(i) {
slides.forEach((s, idx) => s.classList.toggle('active', idx === i));
document.querySelectorAll('.slideshow .dot').forEach((d, idx) =>
d.classList.toggle('active', idx === i));
}
function next() { current = (current + 1) % slides.length; show(current); }
function prev() { current = (current - 1 + slides.length) % slides.length; show(current); }
show(0);
setInterval(next, 3000);
// Wire up optional buttons / dots if present:
document.querySelector('.slideshow .next')?.addEventListener('click', next);
document.querySelector('.slideshow .prev')?.addEventListener('click', prev);
document.querySelectorAll('.slideshow .dot').forEach((d, i) =>
d.addEventListener('click', () => { current = i; show(i); }));
<button class="prev">, <button class="next">, and <span class="dot"> elements inside .slideshow.Swipe or click through items
A scroll-snapping track of cards with arrow buttons. The snap and scroll are CSS; the arrows scroll the track with a line of JavaScript.
HTML
<div class="carousel">
<button class="arrow left">‹</button>
<div class="track">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
</div>
<button class="arrow right">›</button>
</div>
CSS
.track {
display: flex; gap: 10px;
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.track .item {
flex: 0 0 60%; scroll-snap-align: center;
height: 120px; border-radius: 10px;
display: grid; place-items: center;
color: #fff; font-weight: 800;
background: linear-gradient(135deg, #38bdf8, #9b7bff);
}
.carousel { position: relative; }
.arrow {
position: absolute; top: 50%; transform: translateY(-50%);
width: 36px; height: 36px; border-radius: 50%; border: none;
background: #fff; box-shadow: 0 4px 12px rgba(0,0,0,.15); cursor: pointer;
}
.arrow.left { left: -6px; } .arrow.right { right: -6px; }
JavaScript — arrows scroll the track
// Each arrow scrolls the track by the width of one card (+ gap).
document.querySelectorAll('.carousel').forEach(function (car) {
const track = car.querySelector('.track');
const step = () => track.querySelector('.item').offsetWidth + 10;
car.querySelector('.arrow.left').addEventListener('click', function () {
track.scrollBy({ left: -step(), behavior: 'smooth' });
});
car.querySelector('.arrow.right').addEventListener('click', function () {
track.scrollBy({ left: step(), behavior: 'smooth' });
});
});
Before vs. after
Two stacked images; the top one is clipped to a draggable width to reveal what's beneath. This version uses the CSS resize handle — drag the bottom-right corner.
Drag the corner handle to wipe between the two.
HTML
<div class="compare"> <img class="after" src="after.jpg" alt="After"> <div class="before"><img src="before.jpg" alt="Before"></div> </div>
CSS
.compare { position: relative; overflow: hidden; }
.compare .after, .compare .before img { width: 100%; }
.compare .before {
position: absolute; inset: 0;
width: 55%;
overflow: hidden;
resize: horizontal; /* drag handle to reveal */
}
JavaScript — optional center drag handle
// For a polished center handle, add <div class="handle"></div> inside .compare
// and drive .before width from pointer events. Drop the CSS `resize` rule.
const compare = document.querySelector('.compare');
const before = compare.querySelector('.before');
const handle = compare.querySelector('.handle');
let dragging = false;
function setPosition(clientX) {
const rect = compare.getBoundingClientRect();
let pct = ((clientX - rect.left) / rect.width) * 100;
pct = Math.max(0, Math.min(100, pct));
before.style.width = pct + '%';
if (handle) handle.style.left = pct + '%';
}
handle?.addEventListener('pointerdown', e => { dragging = true; handle.setPointerCapture(e.pointerId); });
handle?.addEventListener('pointerup', e => { dragging = false; });
handle?.addEventListener('pointermove', e => { if (dragging) setPosition(e.clientX); });
compare.addEventListener('click', e => setPosition(e.clientX));
CSS — the center handle
.compare .handle {
position: absolute; top: 0; bottom: 0; left: 55%;
width: 4px; background: #fff; cursor: ew-resize;
transform: translateX(-50%);
box-shadow: 0 0 0 1px rgba(0,0,0,.2);
}
.compare .handle::after {
content: ''; position: absolute; top: 50%; left: 50%;
width: 28px; height: 28px; border-radius: 50%; background: #fff;
transform: translate(-50%, -50%); box-shadow: 0 2px 8px rgba(0,0,0,.25);
}
width from pointer events using the JavaScript above — add <div class="handle"></div> inside .compare and drop the CSS resize rule.An episode with a player
Cover art, title, and the native <audio> player — no library needed for playback controls.
HTML
<article class="podcast">
<img class="cover" src="cover.jpg" alt="">
<div>
<p class="title">Episode 12 — Learning CSS</p>
<p class="meta">The Web Design Show · 38 min</p>
</div>
<audio controls src="ep12.mp3"></audio>
</article>
CSS
.podcast { display: flex; gap: 14px; align-items: center;
border: 1px solid #e5e7eb; border-radius: 12px; padding: 14px; }
.cover { width: 64px; height: 64px; border-radius: 10px; }
.podcast audio { width: 100%; }
A responsive thumbnail grid
A gallery is just a CSS grid of images with object-fit: cover so every thumbnail is a clean, equal tile — the grid reflows on its own as the screen narrows.
HTML
<div class="gallery"> <img src="1.jpg" alt="Gallery photo 1"> <img src="2.jpg" alt="Gallery photo 2"> <img src="3.jpg" alt="Gallery photo 3"> <img src="4.jpg" alt="Gallery photo 4"> <img src="5.jpg" alt="Gallery photo 5"> <img src="6.jpg" alt="Gallery photo 6"> </div>
CSS
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 10px;
}
.gallery img { width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 10px; }
Click a thumbnail to enlarge
A lightbox opens a full-size image over a dimmed backdrop. This version is pure CSS using :target — click a thumbnail, then the × to close.
HTML
<div class="gallery"> <a href="#lb1"><img src="thumb1.jpg" alt="Open photo 1"></a> <a href="#lb2"><img src="thumb2.jpg" alt="Open photo 2"></a> <a href="#lb3"><img src="thumb3.jpg" alt="Open photo 3"></a> </div> <div class="lightbox" id="lb1"> <a href="#" class="close">×</a> <img src="full1.jpg" alt="Photo 1 enlarged"> </div> <div class="lightbox" id="lb2"> <a href="#" class="close">×</a> <img src="full2.jpg" alt="Photo 2 enlarged"> </div> <div class="lightbox" id="lb3"> <a href="#" class="close">×</a> <img src="full3.jpg" alt="Photo 3 enlarged"> </div>
CSS
/* thumbnail grid */
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; }
.gallery > a img { width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 10px; cursor: pointer; display: block; }
/* the lightbox overlay */
.lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.85);
display: none; align-items: center; justify-content: center; }
.lightbox:target { display: flex; } /* show when its id is the URL hash */
.lightbox img { max-width: 86%; max-height: 80%; }
.lightbox .close { position: absolute; top: 18px; right: 24px; color: #fff; font-size: 2.2rem; text-decoration: none; }
JavaScript — click-to-open / click-to-close alternative
// JS version: clicking any .gallery img opens a single overlay with that image,
// clicking the overlay (or pressing Esc) closes it. Replaces the :target trick.
// HTML for the overlay:
// <div class="lightbox-overlay"><img alt=""><span class="close">×</span></div>
const overlay = document.querySelector('.lightbox-overlay');
const overlayImg = overlay.querySelector('img');
document.querySelectorAll('.gallery img').forEach(img => {
img.addEventListener('click', () => {
overlayImg.src = img.dataset.full || img.src;
overlayImg.alt = img.alt;
overlay.classList.add('open');
});
});
function close() { overlay.classList.remove('open'); }
overlay.addEventListener('click', close);
document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); });
CSS — for the JS overlay
.lightbox-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.85);
display: none; align-items: center; justify-content: center; z-index: 9999;
}
.lightbox-overlay.open { display: flex; }
.lightbox-overlay img { max-width: 86%; max-height: 80%; border-radius: 10px; }
.lightbox-overlay .close {
position: absolute; top: 18px; right: 24px;
color: #fff; font-size: 2.2rem; cursor: pointer;
}
data-full, add the JavaScript above.Play sound with the native control
For music, narration, or an interview clip, the HTML5 <audio> element gives you a full player — play/pause, scrubber, volume — with one tag and no JavaScript. Press play.
HTML
<div class="player">
<div class="head">
<div class="art"><i class="ph ph-music-notes"></i></div>
<div class="meta">
<p class="title">Episode 12 — Design Systems</p>
<p class="show">The Web Studio Podcast · 3:14</p>
</div>
</div>
<audio controls preload="none">
<source src="episode-12.mp3" type="audio/mpeg">
<source src="episode-12.ogg" type="audio/ogg">
Your browser doesn't support the audio element.
</audio>
</div>
CSS
.player { max-width: 460px; background: #fff; border: 1px solid #e5e7eb;
border-radius: 12px; padding: 16px; }
.player .head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.player .art { width: 46px; height: 46px; border-radius: 10px; background: #38bdf8;
display: grid; place-items: center; color: #fff; font-size: 1.4rem; }
.player .title { font-weight: 700; color: #111; font-size: .92rem; }
.player .show { font-size: .78rem; color: #6b7280; }
.player audio { width: 100%; }
Tips
controls <!-- shows the built-in player UI --> preload="none" <!-- don't download until the user presses play --> loop <!-- repeat automatically (good for ambience) --> /* Provide 2 formats (mp3 + ogg) so every browser has one it can play. */
audio.play(), audio.currentTime). The Multimedia tutorial covers audio and video together.Play a video file with built-in controls
The native <video> element gives you play/pause, a scrubber, volume, and fullscreen for free. Add a poster image so something shows before play. Press play.
HTML
<video controls preload="none" poster="thumbnail.jpg"> <source src="movie.mp4" type="video/mp4"> <source src="movie.webm" type="video/webm"> Your browser doesn't support the video tag. </video>
CSS — keep it responsive
video { width: 100%; max-width: 520px; border-radius: 12px; display: block; }
Video behind your hero text
A muted, auto-playing, looping video sits behind a heading with a dark overlay for legibility. The video must be muted for browsers to allow autoplay, and playsinline so phones don't force fullscreen.
Build for the web
A looping background sets the mood behind your message.
HTML
<div class="hero">
<video autoplay muted loop playsinline poster="fallback.jpg">
<source src="bg.mp4" type="video/mp4">
</video>
<div class="veil"></div>
<div class="content">
<h1>Build for the web</h1>
<p>A looping background sets the mood behind your message.</p>
</div>
</div>
CSS
.hero { position: relative; overflow: hidden; display: grid; place-items: center; }
.hero video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.hero .veil { position: absolute; inset: 0; background: rgba(15,23,42,.55); } /* legibility */
.hero .content { position: relative; z-index: 1; color: #fff; }
poster, and respect @media (prefers-reduced-motion: reduce) by pausing the video for users who ask for less motion.Drop in a YouTube video responsively
YouTube gives you an <iframe> under Share → Embed. Wrap it in a 16:9 box so it scales with the page instead of staying a fixed size.
HTML
<div class="embed">
<iframe src="https://www.youtube.com/embed/VIDEO_ID"
title="YouTube video" loading="lazy" allowfullscreen></iframe>
</div>
CSS — the responsive 16:9 wrapper
.embed { position: relative; width: 100%; aspect-ratio: 16 / 9; }
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
VIDEO_ID in the URL: youtube.com/watch?v=aqz-KE-bpKQ. Use youtube-nocookie.com for a privacy-friendlier embed.The whole thing as one file
A complete media-rich page — a background-video hero, an auto-advancing slideshow, a scroll-snapping carousel, a responsive photo gallery with a pure-CSS lightbox, a native audio player, a self-hosted video, and an embedded YouTube clip — combined into one standalone HTML file. Copy it into a new media.html, open it in a browser, and it just works — no libraries, no build step. Here's the live result, then the full code.
Live preview
Complete HTML — copy this whole file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Studio Reel — Media Gallery</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1f2433; background: #f6f7fb; line-height: 1.6; }
h2 { font-size: 1.4rem; margin-bottom: 4px; color: #111; }
.lead { color: #6b7280; margin-bottom: 16px; font-size: .95rem; }
.wrap { max-width: 860px; margin: 0 auto; padding: 0 20px; }
section.block { padding: 40px 0; border-bottom: 1px solid #e5e7eb; }
/* ===== Background-video hero ===== */
.hero { position: relative; height: 320px; overflow: hidden; display: grid; place-items: center; text-align: center; }
.hero video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.hero .veil { position: absolute; inset: 0; background: rgba(15,23,42,.6); }
.hero .inner { position: relative; z-index: 1; color: #fff; padding: 0 20px; }
.hero h1 { font-size: clamp(1.8rem, 6vw, 3rem); font-weight: 800; letter-spacing: -.02em; }
.hero p { opacity: .92; margin-top: 8px; }
/* ===== Slideshow ===== */
.slideshow { position: relative; height: 200px; border-radius: 12px; overflow: hidden; }
.slide { position: absolute; inset: 0; display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 1.6rem; opacity: 0; animation: fade 9s infinite; }
.slide:nth-child(1) { background: linear-gradient(135deg,#38bdf8,#0ea5e9); animation-delay: 0s; }
.slide:nth-child(2) { background: linear-gradient(135deg,#9b7bff,#7c3aed); animation-delay: 3s; }
.slide:nth-child(3) { background: linear-gradient(135deg,#22c55e,#15803d); animation-delay: 6s; }
@keyframes fade { 0%{opacity:0} 4%{opacity:1} 33%{opacity:1} 37%{opacity:0} 100%{opacity:0} }
/* ===== Carousel ===== */
.carousel { position: relative; }
.track { display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: 4px; }
.track .item { flex: 0 0 60%; scroll-snap-align: center; height: 150px; border-radius: 12px; display: grid; place-items: center; color: #fff; font-weight: 800; font-size: 1.3rem; background: linear-gradient(135deg,#38bdf8,#9b7bff); }
.arrow { position: absolute; top: 50%; transform: translateY(-50%); width: 38px; height: 38px; border-radius: 50%; border: none; background: #fff; box-shadow: 0 4px 12px rgba(0,0,0,.15); cursor: pointer; font-size: 1.1rem; }
.arrow.left { left: -8px; } .arrow.right { right: -8px; }
/* ===== Gallery + lightbox ===== */
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; }
.gallery > a img { width: 100%; aspect-ratio: 1/1; object-fit: cover; border-radius: 10px; cursor: pointer; display: block; transition: opacity .15s; }
.gallery > a:hover img { opacity: .85; }
.lightbox { position: fixed; inset: 0; background: rgba(0,0,0,.88); display: none; align-items: center; justify-content: center; z-index: 999; }
.lightbox:target { display: flex; }
.lightbox img { max-width: 86%; max-height: 82%; border-radius: 10px; }
.lightbox .close { position: absolute; top: 18px; right: 26px; color: #fff; font-size: 2.4rem; text-decoration: none; line-height: 1; }
/* ===== Audio player ===== */
.player { max-width: 460px; background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; }
.player .head { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.player .art { width: 46px; height: 46px; border-radius: 10px; background: #38bdf8; display: grid; place-items: center; color: #fff; font-size: 1.4rem; }
.player .title { font-weight: 700; color: #111; font-size: .92rem; }
.player .show { font-size: .78rem; color: #6b7280; }
.player audio { width: 100%; }
/* ===== Video + embed ===== */
video.file { width: 100%; max-width: 560px; border-radius: 12px; display: block; background: #000; }
.embed { position: relative; width: 100%; max-width: 560px; aspect-ratio: 16 / 9; border-radius: 12px; overflow: hidden; background: #000; }
.embed iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
@media (prefers-reduced-motion: reduce) {
.slide { animation: none; }
.slide:nth-child(1) { opacity: 1; }
}
</style>
</head>
<body>
<!-- Background-video hero -->
<header class="hero">
<video autoplay muted loop playsinline poster="https://images.unsplash.com/photo-1470770841072-f978cf4d019e?w=900&q=70">
<source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
</video>
<div class="veil"></div>
<div class="inner">
<h1>Studio Reel</h1>
<p>Photos, audio, and video — all on one page.</p>
</div>
</header>
<main>
<!-- Slideshow -->
<section class="block"><div class="wrap">
<h2>Featured</h2>
<p class="lead">An auto-advancing slideshow, pure CSS.</p>
<div class="slideshow">
<div class="slide">Spring Collection</div>
<div class="slide">Behind the Scenes</div>
<div class="slide">On Location</div>
</div>
</div></section>
<!-- Carousel -->
<section class="block"><div class="wrap">
<h2>Recent Shoots</h2>
<p class="lead">A scroll-snapping carousel — use the arrows or swipe.</p>
<div class="carousel">
<button class="arrow left" aria-label="Previous">‹</button>
<div class="track">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
</div>
<button class="arrow right" aria-label="Next">›</button>
</div>
</div></section>
<!-- Gallery + lightbox -->
<section class="block"><div class="wrap">
<h2>Gallery</h2>
<p class="lead">A responsive thumbnail grid — click any photo to enlarge.</p>
<div class="gallery">
<a href="#shot1"><img src="https://picsum.photos/seed/reel1/260/260" alt="Open photo 1"></a>
<a href="#shot2"><img src="https://picsum.photos/seed/reel2/260/260" alt="Open photo 2"></a>
<a href="#shot3"><img src="https://picsum.photos/seed/reel3/260/260" alt="Open photo 3"></a>
<a href="#shot4"><img src="https://picsum.photos/seed/reel4/260/260" alt="Open photo 4"></a>
<a href="#shot5"><img src="https://picsum.photos/seed/reel5/260/260" alt="Open photo 5"></a>
<a href="#shot6"><img src="https://picsum.photos/seed/reel6/260/260" alt="Open photo 6"></a>
</div>
<div class="lightbox" id="shot1"><a href="#" class="close" aria-label="Close">×</a><img src="https://picsum.photos/seed/reel1/900/600" alt="Photo 1 enlarged"></div>
<div class="lightbox" id="shot2"><a href="#" class="close" aria-label="Close">×</a><img src="https://picsum.photos/seed/reel2/900/600" alt="Photo 2 enlarged"></div>
<div class="lightbox" id="shot3"><a href="#" class="close" aria-label="Close">×</a><img src="https://picsum.photos/seed/reel3/900/600" alt="Photo 3 enlarged"></div>
<div class="lightbox" id="shot4"><a href="#" class="close" aria-label="Close">×</a><img src="https://picsum.photos/seed/reel4/900/600" alt="Photo 4 enlarged"></div>
<div class="lightbox" id="shot5"><a href="#" class="close" aria-label="Close">×</a><img src="https://picsum.photos/seed/reel5/900/600" alt="Photo 5 enlarged"></div>
<div class="lightbox" id="shot6"><a href="#" class="close" aria-label="Close">×</a><img src="https://picsum.photos/seed/reel6/900/600" alt="Photo 6 enlarged"></div>
</div></section>
<!-- Audio -->
<section class="block"><div class="wrap">
<h2>Listen</h2>
<p class="lead">The native audio player — press play.</p>
<div class="player">
<div class="head">
<div class="art">♫</div>
<div>
<p class="title">Episode 12 — Design Systems</p>
<p class="show">The Web Studio Podcast · 3:14</p>
</div>
</div>
<audio controls preload="none">
<source src="https://www.w3schools.com/html/horse.mp3" type="audio/mpeg">
Your browser doesn't support the audio element.
</audio>
</div>
</div></section>
<!-- Video -->
<section class="block"><div class="wrap">
<h2>Watch</h2>
<p class="lead">A self-hosted video with built-in controls.</p>
<video class="file" controls preload="none" poster="https://images.unsplash.com/photo-1535016120720-40c646be5580?w=720&q=70">
<source src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
Your browser doesn't support the video tag.
</video>
</div></section>
<!-- YouTube embed -->
<section class="block" style="border-bottom:none;"><div class="wrap">
<h2>From the Channel</h2>
<p class="lead">An embedded YouTube video in a responsive 16:9 wrapper.</p>
<div class="embed">
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ" title="YouTube video" loading="lazy" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
</div></section>
</main>
<script>
// Carousel arrows scroll the track by one card width
document.querySelectorAll('.carousel').forEach(function (car) {
var track = car.querySelector('.track');
var step = function () { return track.querySelector('.item').offsetWidth + 10; };
car.querySelector('.arrow.left').addEventListener('click', function () {
track.scrollBy({ left: -step(), behavior: 'smooth' });
});
car.querySelector('.arrow.right').addEventListener('click', function () {
track.scrollBy({ left: step(), behavior: 'smooth' });
});
});
</script>
</body>
</html>
Keep building
See Images, Gallery Pages, Video, and Video & Multimedia — or browse the full Component Library.