Open demo

CSS code

HTML
<div class="w">
  <div class="stage">
    <div class="big b1 g1"><p class="cap"><b>Studio session</b><small>Shot on 35mm · March</small></p></div>
    <div class="big b2 g2"><p class="cap"><b>Coastal frequencies</b><small>Album artwork · April</small></p></div>
    <div class="big b3 g3"><p class="cap"><b>Summer campaign</b><small>Outdoor and print · June</small></p></div>
    <div class="big b4 g4"><p class="cap"><b>Field notes</b><small>Editorial spread · August</small></p></div>
  </div>
  <div class="thumbs" role="radiogroup" aria-label="Choose an image">
    <input type="radio" name="m" id="m1" checked><label class="g1" for="m1"><span class="sr-only">Studio session</span></label>
    <input type="radio" name="m" id="m2"><label class="g2" for="m2"><span class="sr-only">Coastal frequencies</span></label>
    <input type="radio" name="m" id="m3"><label class="g3" for="m3"><span class="sr-only">Summer campaign</span></label>
    <input type="radio" name="m" id="m4"><label class="g4" for="m4"><span class="sr-only">Field notes</span></label>
  </div>
</div>
CSS
/* Media tabs, CSS only: the thumbnails are a radio group; :has() shows the large frame for the checked one.
   Gradients stand in for images. A dark scrim under the caption keeps the white text readable. */
.w{width:100%;max-width:520px;padding:20px;display:flex;flex-direction:column;gap:14px;background:#fff;
  border:1px solid #e3e8f2;border-radius:16px;box-shadow:0 16px 38px -24px rgba(16,24,48,.24)}
.stage{aspect-ratio:16/10;border-radius:12px;position:relative;overflow:hidden}
.big{position:absolute;inset:0;display:none;align-items:end;padding:20px;color:#fff}
.big::before{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(15,23,42,.55),transparent 60%)}
.big .cap{position:relative}
.big b{display:block;font-size:1.05rem;letter-spacing:-.01em;text-shadow:0 2px 12px rgba(0,0,0,.45)}
.big small{display:block;font-size:.8rem;opacity:.92;margin-top:3px;text-shadow:0 1px 8px rgba(0,0,0,.5)}
.thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;position:relative}
.thumbs input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
.thumbs label{aspect-ratio:16/10;border-radius:9px;cursor:pointer;transition:box-shadow .15s,transform .15s}
.thumbs label:hover{transform:translateY(-2px)}
.thumbs input:checked+label{box-shadow:0 0 0 2px #fff,0 0 0 4px #4f46e5}
.thumbs input:focus-visible+label{outline:2px solid #16203a;outline-offset:6px}
.g1{background:linear-gradient(140deg,#6366f1,#a855f7)}
.g2{background:linear-gradient(140deg,#0ea5e9,#22d3ee)}
.g3{background:linear-gradient(140deg,#f59e0b,#ef4444)}
.g4{background:linear-gradient(140deg,#16a34a,#84cc16)}
.w:has(#m1:checked) .b1,.w:has(#m2:checked) .b2,.w:has(#m3:checked) .b3,.w:has(#m4:checked) .b4{display:flex}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (prefers-reduced-motion:reduce){.thumbs label{transition:none}.thumbs label:hover{transform:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div class="w">
  <div class="tab-content ratio stage">
    <div class="tab-pane big g1 active" id="shot-studio" role="tabpanel" aria-labelledby="shot-studio-tab"><p class="cap"><b>Studio session</b><small>Shot on 35mm · March</small></p></div>
    <div class="tab-pane big g2" id="shot-coastal" role="tabpanel" aria-labelledby="shot-coastal-tab"><p class="cap"><b>Coastal frequencies</b><small>Album artwork · April</small></p></div>
    <div class="tab-pane big g3" id="shot-summer" role="tabpanel" aria-labelledby="shot-summer-tab"><p class="cap"><b>Summer campaign</b><small>Outdoor and print · June</small></p></div>
    <div class="tab-pane big g4" id="shot-field" role="tabpanel" aria-labelledby="shot-field-tab"><p class="cap"><b>Field notes</b><small>Editorial spread · August</small></p></div>
  </div>
  <div class="nav thumbs" role="tablist" aria-label="Choose an image">
    <button class="g1 active" id="shot-studio-tab" data-bs-toggle="tab" data-bs-target="#shot-studio" type="button" role="tab" aria-controls="shot-studio" aria-selected="true" aria-label="Studio session"></button>
    <button class="g2" id="shot-coastal-tab" data-bs-toggle="tab" data-bs-target="#shot-coastal" type="button" role="tab" aria-controls="shot-coastal" aria-selected="false" tabindex="-1" aria-label="Coastal frequencies"></button>
    <button class="g3" id="shot-summer-tab" data-bs-toggle="tab" data-bs-target="#shot-summer" type="button" role="tab" aria-controls="shot-summer" aria-selected="false" tabindex="-1" aria-label="Summer campaign"></button>
    <button class="g4" id="shot-field-tab" data-bs-toggle="tab" data-bs-target="#shot-field" type="button" role="tab" aria-controls="shot-field" aria-selected="false" tabindex="-1" aria-label="Field notes"></button>
  </div>
</div>
CSS
/* Media tabs: the thumbnails are a Bootstrap .nav of tab buttons driven by the Tab plugin; the stage is a
   .tab-content inside a 16:10 .ratio. Gradients stand in for images. */
.w{width:100%;max-width:520px;padding:20px;display:flex;flex-direction:column;gap:14px;background:#fff;
  border:1px solid #e3e8f2;border-radius:16px;box-shadow:0 16px 38px -24px rgba(16,24,48,.24)}
.stage{--bs-aspect-ratio:62.5%;border-radius:12px;overflow:hidden}
.big{color:#fff}
.big::before{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(15,23,42,.55),transparent 60%)}
.big .cap{position:absolute;left:20px;right:20px;bottom:20px;margin:0}
.big b{display:block;font-size:1.05rem;letter-spacing:-.01em;text-shadow:0 2px 12px rgba(0,0,0,.45)}
.big small{display:block;font-size:.8rem;opacity:.92;margin-top:3px;text-shadow:0 1px 8px rgba(0,0,0,.5)}
.thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.thumbs [role=tab]{aspect-ratio:16/10;border:0;padding:0;border-radius:9px;transition:box-shadow .15s,transform .15s}
.thumbs [role=tab]:hover{transform:translateY(-2px)}
.thumbs [role=tab].active{box-shadow:0 0 0 2px #fff,0 0 0 4px #4f46e5}
.thumbs [role=tab]:focus-visible{outline:2px solid #16203a;outline-offset:6px}
.g1{background:linear-gradient(140deg,#6366f1,#a855f7)}
.g2{background:linear-gradient(140deg,#0ea5e9,#22d3ee)}
.g3{background:linear-gradient(140deg,#f59e0b,#ef4444)}
.g4{background:linear-gradient(140deg,#16a34a,#84cc16)}
@media (prefers-reduced-motion:reduce){.thumbs [role=tab]{transition:none}.thumbs [role=tab]:hover{transform:none}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="flex w-full max-w-[520px] flex-col gap-3.5 rounded-2xl border border-tab-line bg-white p-5 shadow-[0_16px_38px_-24px_rgba(16,24,48,.24)]">
  <div class="relative aspect-[16/10] overflow-hidden rounded-xl">
    <div id="shot-studio" role="tabpanel" aria-labelledby="shot-studio-tab" class="absolute inset-0 flex items-end bg-[linear-gradient(to_top,rgba(15,23,42,.55),transparent_60%),linear-gradient(140deg,#6366f1,#a855f7)] p-5 text-white"><p><b class="block text-[1.05rem] tracking-[-.01em] [text-shadow:0_2px_12px_rgba(0,0,0,.45)]">Studio session</b><small class="mt-[3px] block text-[.8rem] opacity-[.92] [text-shadow:0_1px_8px_rgba(0,0,0,.5)]">Shot on 35mm · March</small></p></div>
    <div id="shot-coastal" role="tabpanel" aria-labelledby="shot-coastal-tab" hidden class="absolute inset-0 flex items-end bg-[linear-gradient(to_top,rgba(15,23,42,.55),transparent_60%),linear-gradient(140deg,#0ea5e9,#22d3ee)] p-5 text-white"><p><b class="block text-[1.05rem] tracking-[-.01em] [text-shadow:0_2px_12px_rgba(0,0,0,.45)]">Coastal frequencies</b><small class="mt-[3px] block text-[.8rem] opacity-[.92] [text-shadow:0_1px_8px_rgba(0,0,0,.5)]">Album artwork · April</small></p></div>
    <div id="shot-summer" role="tabpanel" aria-labelledby="shot-summer-tab" hidden class="absolute inset-0 flex items-end bg-[linear-gradient(to_top,rgba(15,23,42,.55),transparent_60%),linear-gradient(140deg,#f59e0b,#ef4444)] p-5 text-white"><p><b class="block text-[1.05rem] tracking-[-.01em] [text-shadow:0_2px_12px_rgba(0,0,0,.45)]">Summer campaign</b><small class="mt-[3px] block text-[.8rem] opacity-[.92] [text-shadow:0_1px_8px_rgba(0,0,0,.5)]">Outdoor and print · June</small></p></div>
    <div id="shot-field" role="tabpanel" aria-labelledby="shot-field-tab" hidden class="absolute inset-0 flex items-end bg-[linear-gradient(to_top,rgba(15,23,42,.55),transparent_60%),linear-gradient(140deg,#16a34a,#84cc16)] p-5 text-white"><p><b class="block text-[1.05rem] tracking-[-.01em] [text-shadow:0_2px_12px_rgba(0,0,0,.45)]">Field notes</b><small class="mt-[3px] block text-[.8rem] opacity-[.92] [text-shadow:0_1px_8px_rgba(0,0,0,.5)]">Editorial spread · August</small></p></div>
  </div>
  <div class="grid grid-cols-4 gap-2.5" role="tablist" aria-label="Choose an image">
    <button type="button" role="tab" id="shot-studio-tab" aria-controls="shot-studio" aria-selected="true" aria-label="Studio session" class="aspect-[16/10] cursor-pointer rounded-[9px] bg-[linear-gradient(140deg,#6366f1,#a855f7)] transition-[box-shadow,translate] duration-150 hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-offset-[6px] focus-visible:outline-tab-ink aria-selected:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tab-accent)] motion-reduce:transition-none motion-reduce:hover:translate-y-0"></button>
    <button type="button" role="tab" id="shot-coastal-tab" aria-controls="shot-coastal" aria-selected="false" tabindex="-1" aria-label="Coastal frequencies" class="aspect-[16/10] cursor-pointer rounded-[9px] bg-[linear-gradient(140deg,#0ea5e9,#22d3ee)] transition-[box-shadow,translate] duration-150 hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-offset-[6px] focus-visible:outline-tab-ink aria-selected:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tab-accent)] motion-reduce:transition-none motion-reduce:hover:translate-y-0"></button>
    <button type="button" role="tab" id="shot-summer-tab" aria-controls="shot-summer" aria-selected="false" tabindex="-1" aria-label="Summer campaign" class="aspect-[16/10] cursor-pointer rounded-[9px] bg-[linear-gradient(140deg,#f59e0b,#ef4444)] transition-[box-shadow,translate] duration-150 hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-offset-[6px] focus-visible:outline-tab-ink aria-selected:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tab-accent)] motion-reduce:transition-none motion-reduce:hover:translate-y-0"></button>
    <button type="button" role="tab" id="shot-field-tab" aria-controls="shot-field" aria-selected="false" tabindex="-1" aria-label="Field notes" class="aspect-[16/10] cursor-pointer rounded-[9px] bg-[linear-gradient(140deg,#16a34a,#84cc16)] transition-[box-shadow,translate] duration-150 hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-offset-[6px] focus-visible:outline-tab-ink aria-selected:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tab-accent)] motion-reduce:transition-none motion-reduce:hover:translate-y-0"></button>
  </div>
</div>
CSS
@theme {
  --color-tab-ink: #16203a;
  --color-tab-line: #e3e8f2;
  --color-tab-accent: #4f46e5;
}
JavaScript
// WAI-ARIA tabs: click or arrow keys select a tab, Home/End jump to the ends, only the selected tab is in the Tab order.
for (const list of document.querySelectorAll('[role=tablist]')) {
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const select = tab => tabs.forEach(t => {
    const on = t === tab;
    t.setAttribute('aria-selected', on);
    t.tabIndex = on ? 0 : -1;
    document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
  });
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t) select(t); });
  list.addEventListener('keydown', e => {
    const i = tabs.indexOf(e.target);
    const n = { ArrowLeft: i - 1, ArrowUp: i - 1, ArrowRight: i + 1, ArrowDown: i + 1, Home: 0, End: tabs.length - 1 }[e.key];
    if (n === undefined || i < 0) return;
    e.preventDefault();
    const t = tabs[(n + tabs.length) % tabs.length];
    t.focus();
    select(t);
  });
}

About this set

A media viewer built as tabs: a large 16:10 frame with a caption and a row of four thumbnails underneath, where each thumbnail selects its frame. Gradients stand in for photos so the set has no image files; swap them for img or background images. Use it for portfolio pieces, product shots, case studies or album artwork where the thumbnails are the navigation. The selected thumbnail gets a two-ring indigo outline, and a dark scrim at the bottom of the frame keeps the white caption readable on any picture. The plain CSS version is a radio group: each thumbnail is a label with visually hidden text, and :has() shows the frame for the checked radio. The Bootstrap version is a .nav of tab buttons driven by the Tab plugin, with the frames in a .tab-content inside a .ratio set to 16:10. The Tailwind version uses the same markup with utilities and a small WAI-ARIA tabs script. Thumbnails lift slightly on hover, and that movement stops under reduced motion.

What’s included

  • Large 16:10 frame with caption and a four-thumbnail strip
  • Thumbnails carry text names for screen readers
  • Dark scrim keeps white caption text readable on any image
  • Selected ring and keyboard focus ring are drawn differently
  • Bootstrap version uses .ratio and the Tab plugin

Accessibility

In the plain CSS version the tabs are a radio group, not ARIA tabs: the radios sit inside an element with role="radiogroup" and an aria-label, each takes its name from its visible label, Tab enters the group and the arrow keys move the selection (radios have no Home or End). In the Bootstrap and Tailwind versions they follow the WAI-ARIA tabs pattern: role tablist, tab and tabpanel, aria-selected, aria-controls, a roving tabindex, arrow keys plus Home and End. Each thumbnail is named by visually hidden text or an aria-label. The selected thumbnail has an indigo ring and keyboard focus adds a separate dark outline around it, and the hover lift is removed under prefers-reduced-motion.

Customise it

Replace the .g1 to .g4 gradients with your images. The selected ring is the box-shadow using #4f46e5 on the checked label (plain CSS), on .thumbs .active (Bootstrap) or in the aria-selected:shadow utility (Tailwind); the frame ratio is aspect-ratio, --bs-aspect-ratio or aspect-[16/10].