← Component Library
Media Components

Media Patterns

Slideshows, carousels, comparison sliders, audio, and immersive media. Each with a live demo and the HTML & CSS.

01 · Slideshow

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

Slide 1
Slide 2
Slide 3

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); }));
For controls (next/prev, pause), swap the CSS timer for the JavaScript above and add <button class="prev">, <button class="next">, and <span class="dot"> elements inside .slideshow.
03 · Image Comparison Slider

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.

AFTER
BEFORE

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);
}
For a polished center handle you can drag anywhere, drive the width from pointer events using the JavaScript above — add <div class="handle"></div> inside .compare and drop the CSS resize rule.
04 · Podcast Section

An episode with a player

Cover art, title, and the native <audio> player — no library needed for playback controls.

Episode 12 — Learning CSS
The Web Design Show · 38 min

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%; }
07 · Audio Player

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.

Episode 12 — Design Systems
The Web Studio Podcast · 3:14

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. */
Want a fully custom player — your own buttons and waveform? Hide the default UI and drive it with the JavaScript media API (audio.play(), audio.currentTime). The Multimedia tutorial covers audio and video together.
08 · Video Player

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; }
Use this for self-hosted video. For YouTube/Vimeo, embed their iframe instead (§10) so you don't host the file or bandwidth.
09 · Background Video

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; }
Performance & access: keep the file small, always provide a poster, and respect @media (prefers-reduced-motion: reduce) by pausing the video for users who ask for less motion.
10 · Embedded YouTube Video

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; }
Find VIDEO_ID in the URL: youtube.com/watch?v=aqz-KE-bpKQ. Use youtube-nocookie.com for a privacy-friendlier embed.
11 · Full Page

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">&lsaquo;</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">&rsaquo;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&times;</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">&#9835;</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>
That's a real, standalone page. The live preview above is rendered from the exact code in the box — so what you copy is precisely what you see.
12 · Related

Keep building

See Images, Gallery Pages, Video, and Video & Multimedia — or browse the full Component Library.