Overview

Media elements are the main source of layout shift, wasted bytes, and accessibility failures. Correct <img>, srcset, sizes, loading, and <track> usage prevents most of them before JavaScript runs.

Use srcset and sizes for resolution switching

srcset lists candidate files with intrinsic widths; sizes tells the browser how wide the image will render so it can choose before layout. Without sizes, the browser assumes the image fills the viewport and may fetch the largest candidate.

<img
  src="/hero-800.jpg"
  srcset="/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1200.jpg 1200w"
  sizes="(max-width: 720px) 100vw, 800px"
  width="800" height="450"
  alt="A subway map of Tokyo."
/>

Use <picture> for art direction and format fallback

<picture> wraps <source> elements chosen by media or type, plus a required <img> fallback. Use it to crop differently at different widths. AVIF is Baseline widely available, so format fallback is only needed if you support older browsers; when you do, order sources from newest format to oldest.

<picture>
  <source srcset="/hero.avif" type="image/avif" />
  <img src="/hero.jpg" alt="A subway map of Tokyo." width="800" height="450" />
</picture>

The alt belongs on the <img>, never on <source>.

Always set width and height

The browser derives the aspect ratio from them and reserves space before the bytes arrive, which prevents cumulative layout shift. Use the intrinsic pixel dimensions; CSS can still resize the element and the reserved space scales with it. Apply this to <video> too. See core-web-vitals.

Lazy-load below the fold, prioritize the LCP image

loading="lazy" on images and iframes is Baseline widely available. Never lazy-load anything visible on first paint; it delays the Largest Contentful Paint. Mark the LCP image fetchpriority="high" (newly available since October 2024).

<img src="/hero.jpg" fetchpriority="high" width="1200" height="630" alt="..." />
<img src="/card.jpg" loading="lazy" width="400" height="300" alt="..." />

See html-links-rel for preload when the image is discovered late.

Give video a poster, captions, and sane autoplay

<video src="/demo.mp4" poster="/demo-poster.jpg" width="800" height="450" controls preload="none">
  <track kind="captions" src="/demo.vtt" srclang="en" label="English" />
</video>
  • poster shows a frame before playback.
  • controls keeps native keyboard access; do not replace it with script-only UI.
  • preload="none" avoids fetching video the user may never watch.
  • <track kind="captions">: WCAG 1.2.2 (Level A) requires captions for prerecorded audio in synchronized media.
  • Do not autoplay with sound (SC 1.4.2). Muted background video may autoplay, but if it moves for more than five seconds it needs a pause control (SC 2.2.2). Use muted playsinline so mobile browsers allow it.

Provide a transcript for audio

<audio controls preload="none"><source src="/podcast.mp3" type="audio/mpeg" /></audio>
<a href="/podcast-transcript">Read the transcript</a>

WCAG 1.2.1 (Level A) requires an alternative for prerecorded audio-only content; a linked transcript satisfies it. Do not rely on <track> for audio-only players.

Avoid common pitfalls

  • srcset without sizes on images narrower than the viewport.
  • <picture> used only for resolution switching; a plain <img srcset> does that.
  • Missing alt, or alt on <source>.
  • Lazy-loading the hero image.
  • Video with meaningful audio and no captions.

Next.js generates srcset and sizes for you; see nextjs-image-optimization.