../

Media & embeds

Images, responsive images, video, audio, iframes, SVG and canvas in HTML as of September 2026, with Baseline status from MDN and webstatus.dev. Captions, alt text and embeds are covered here from the markup side; the wider rules are in Accessibility, page-level preloading is in Document head, and drawing with script is in Canvas 2D.

<img> essentials

<img
  src="/img/team-800.jpg"
  alt="Five engineers around a whiteboard sketching a map"
  width="800" height="533"
  loading="lazy" decoding="async">
AttributeUseNotes
srcthe fallback URLalways set it, even with srcset
alttext alternativerequired; alt="" for decoration (see the table below)
width, heightintrinsic size in CSS px, no unitsthe browser derives aspect-ratio from them and reserves space, so no layout shift (CLS)
srcsetcandidate files with w or x descriptorsthe browser picks one
sizesthe rendered width for w candidatesdefaults to 100vw when missing
loadingeager (default) or lazylazy-loading is Baseline widely available (June 2026) for img and iframe
decodingsync, async, auto (default)a hint; the effect is rarely measurable for markup images
fetchpriorityhigh, low, auto (default)Baseline 2024 (October); use high on the LCP image
crossoriginanonymous, use-credentialsneeded to read pixels into a canvas without tainting it
referrerpolicyas for iframerarely needed on images
usemap#map-nameimage maps, see below
ismapserver-side maplegacy; don't use

Set width and height to the file's pixel size and let CSS scale it. The attributes only supply the ratio once CSS gives one dimension:

img, video {
  max-inline-size: 100%;
  block-size: auto;  /* keep the attribute-derived ratio */
}

Alt text by image purpose

The W3C WAI Images Tutorial sorts images by what they do on the page, not by what they show. The same photo needs different alt text in different places.

PurposeExamplealtRule
Informativea photo in a news story, a diagram with a simple pointa short description of the information it addsone or two sentences; no "image of"
Decorativea divider, a background texture, a photo that repeats the adjacent headingalt=""empty, never missing; screen readers skip it
Functionalan image that is the only content of a link or buttonwhat happens: alt="Search", alt="Acme home"describe the action or destination, not the picture
Image of texta logo wordmark, a styled quotethe text itselfavoid images of text otherwise (WCAG 1.4.5)
Complexa chart, a map, an infographica short summary in alt, the full data nearbyput the long description in the page (table, figcaption) and point to it
Groupfive star icons for a ratingalt="4 out of 5 stars" on one, alt="" on the restone alt for the whole set
Redundant with caption or link texta thumbnail next to its own title linkalt=""don't make the screen reader say it twice
<!-- functional: the link's name comes from the alt -->
<a href="/"><img src="logo.svg" alt="Acme home"></a>
 
<!-- complex: short alt, long description in the page -->
<figure>
  <img src="sales.png" alt="Bar chart of 2025 sales by
    quarter; details in the table below"
    aria-describedby="sales-desc">
  <figcaption id="sales-desc">Sales rose each quarter,
    from 1.2 M in Q1 to 2.0 M in Q4.</figcaption>
</figure>
MistakeWhy it fails
alt missingmany screen readers read the file name instead ("IMG underscore 4032 dot jpeg")
alt="image", alt="photo of…"the role already says it is an image
alt repeats the captionread twice
alt on a decorative flourishnoise on every page load
title instead of altnot a reliable text alternative; invisible to touch users
describing a functional image's pixels"magnifying glass" tells nobody that it searches

The alt text renders in place of an image that fails to load; an empty alt renders nothing:

alt-fallback.html
<style>
  img { display: block; margin-block: 6px 14px;
    font-style: italic; color: var(--muted); }
  p { margin: 0; }
</style>
<p>Informative image that failed to load:</p>
<img src="data:," width="300" height="44"
  alt="Line chart: sign-ups doubled from May to June">
<p>Decorative image with <code>alt=""</code>:</p>
<img src="data:," alt="">
<p>Nothing is drawn or read for it.</p>
Result

Loading, decoding & priority

The Largest Contentful Paint (LCP) element is usually a hero image. Google's Core Web Vitals thresholds, measured at the 75th percentile of page loads, are LCP ≤ 2.5 s, INP ≤ 200 ms and CLS ≤ 0.1.

ImageloadingfetchpriorityWhy
The LCP image (hero, first product photo)omit (eager)highlazy-loading it delays LCP: the browser waits for layout before fetching
Other images in the first viewportomitomitthe preload scanner finds them early anyway
Carousel slides 2 onwardslazy or omitlowvisible soon, but not first
Everything below the foldlazyomitsaves bytes the user may never scroll to
Images injected by JSlazyomitand give them width/height
<!-- above the fold: fetched first -->
<img src="hero.avif" alt="…" width="1600" height="900"
  fetchpriority="high">
 
<!-- below the fold: fetched near the viewport -->
<img src="chart.png" alt="…" width="800" height="400"
  loading="lazy">
  • A lazy image with no dimensions is 0 × 0 until it loads, so it may never intersect the viewport and never load inside a scroller. Always give it width and height.
  • <img sizes="auto" loading="lazy"> (use the laid-out width to pick from srcset) is Chromium only: limited availability. Keep an explicit fallback list after auto.
  • loading="lazy" on <video> and <audio> is limited availability; use preload="none".
  • An LCP image referenced only from CSS or JS is invisible to the preload scanner; preload it from the head (see Document head):
<link rel="preload" as="image" fetchpriority="high"
  imagesrcset="hero-800.avif 800w, hero-1600.avif 1600w"
  imagesizes="100vw" type="image/avif">

Responsive images: srcset & sizes

Two problems, two descriptors:

DescriptorProblem it solvesExamplesizes
x (density)the same CSS size on 1×, 2×, 3× screens: logos, avatars, iconssrcset="a.png 1x, a@2x.png 2x"not used
w (width)the image's CSS width changes with the layoutsrcset="a-400.jpg 400w, a-800.jpg 800w"required in practice
<!-- fixed 48 px avatar: density descriptors -->
<img src="ana-48.jpg" alt="Ana Diaz" width="48" height="48"
  srcset="ana-48.jpg 1x, ana-96.jpg 2x, ana-144.jpg 3x">
 
<!-- fluid card image: width descriptors plus sizes -->
<img
  src="card-800.jpg"
  srcset="card-400.jpg 400w, card-800.jpg 800w,
          card-1200.jpg 1200w, card-1600.jpg 1600w"
  sizes="(min-width: 64rem) 20rem,
         (min-width: 40rem) calc(50vw - 2rem),
         calc(100vw - 2rem)"
  width="800" height="600" alt="…">

How the browser picks, step by step:

  1. Evaluate sizes left to right; the first media condition that matches gives the slot width. The last entry has no condition and is the default. rem and em here are the initial font size, normally 16 px.
  2. Multiply the slot width by the device pixel ratio (DPR) to get the pixels needed.
  3. Choose a candidate from srcset. Browsers usually take the smallest one that covers the need, but the spec leaves the final choice to them: a larger file already in the cache, or a smaller one on a slow connection, are both allowed.

Worked through for the card above (64rem = 1024 px, 40rem = 640 px):

DeviceViewportDPRMatching entrySlotNeededPicked
Phone390 px3calc(100vw - 2rem)358 px1074 pxcard-1200.jpg
Tablet800 px2calc(50vw - 2rem)368 px736 pxcard-800.jpg
Laptop1440 px120rem320 px320 pxcard-400.jpg
Laptop, Retina1440 px220rem320 px640 pxcard-800.jpg

Without sizes, the phone would assume 100vw × 3 = 1170 px (same pick) but the laptop would assume 1440 px and download card-1600.jpg for a 320 px slot, 4–5× the bytes.

RuleDetail
sizes describes layout, not filescopy the slot widths from your CSS breakpoints; overestimating wastes bytes, underestimating blurs
Order sizes from the most specific conditionfirst match wins, like if … else if
Don't mix w and xin one srcset that is invalid
4–6 widths is plentye.g. 400, 800, 1200, 1600, 2000; beyond 2× DPR the gains are small
Cap the largest candidate2× the biggest slot is enough for most photos
srcset is a hintnever rely on which file is chosen (analytics, JS logic)

<picture>: art direction & format fallback

<picture> wraps one <img> and any number of <source> elements. The browser takes the first <source> whose media and type both match, then applies its srcset/sizes. The <img> supplies alt, dimensions, loading and the final fallback; it is what renders and what you style.

<picture>
  <source type="image/avif"
    srcset="hero-800.avif 800w, hero-1600.avif 1600w"
    sizes="100vw">
  <source type="image/webp"
    srcset="hero-800.webp 800w, hero-1600.webp 1600w"
    sizes="100vw">
  <img src="hero-1600.jpg" alt="…"
    srcset="hero-800.jpg 800w, hero-1600.jpg 1600w"
    sizes="100vw" width="1600" height="900">
</picture>
UseReach for
Same image, different resolutions<img srcset sizes>; no <picture> needed
Different crop or composition per breakpoint (art direction)<picture> + <source media>
Newer format with fallback<picture> + <source type>
Light and dark versions<source media="(prefers-color-scheme: dark)">
Reduced motion (static instead of animated)<source media="(prefers-reduced-motion: reduce)">
  • Put width and height on each <source> whose crop has a different ratio; all engines have supported it since late 2022.
  • alt goes on the <img> only. <source> has no alt.
  • With media sources the browser must obey the match; with plain srcset it may choose. That is the difference between art direction and resolution switching.

Image formats

FormatCompressionAlphaAnimationBest forSupport
JPEGlossynonophotos; universal fallbackeverywhere
PNGlosslessyesAPNGscreenshots, flat graphics needing exact pixelseverywhere
WebPlossy and losslessyesyesphotos and graphics; MDN cites 25–35% smaller than JPEGBaseline widely available (since 2023)
AVIFlossy and losslessyesyesphotos; MDN cites about 50% smaller than JPEG; slower to encodeBaseline 2024 (January), widely available since July 2026
SVGvector (text, gzips well)yesyes (SMIL, CSS)icons, logos, diagrams, anything drawneverywhere
GIFlossless, 256 colors1-bityesnothing new; use <video> or animated WebP/AVIFeverywhere
  • Default in 2026: AVIF, with WebP or JPEG fallback in <picture>. AVIF is now widely available, so the JPEG fallback matters mainly for old devices and email clients.
  • AVIF has no progressive rendering: a large AVIF appears all at once, a progressive JPEG paints coarse first.
  • Animated GIFs are huge: web.dev's example clip is 3.7 MB as GIF, 551 KB as MP4 and 341 KB as WebM. Use a muted <video autoplay loop muted playsinline> instead.
  • JPEG XL is not Baseline (Safari only); don't serve it without a fallback.
  • Compress at build time or through an image CDN; quality 60–80 is typical for lossy photos. Check visually rather than trusting one number.

Figures & captions

<figure> is self-contained content referenced from the main text: an image, chart, code listing, quote or video. <figcaption> (first or last child) gives it a caption, which browsers use as the figure's accessible name.

<figure>
  <img src="map.png" alt="Route map of the 12 km loop"
    width="800" height="500">
  <figcaption>Fig. 3: The loop starts and ends at the
    north car park.</figcaption>
</figure>
RuleDetail
Caption ≠ altthe caption is visible context for everyone; the alt replaces the image for those who can't see it
Don't repeatif the caption fully describes the image, a short or empty alt is fine
Not every image is a figuredecorative and inline images don't need <figure>
Default marginsfigure has margin: 1em 40px; reset it

An inline SVG chart as a captioned figure. role="img" flattens it into one image for assistive tech, and aria-labelledby names it from its <title>:

figure.html
<style>
  figure { margin: 0; max-inline-size: 320px; }
  figcaption { color: var(--muted); font-size: 13px; }
  rect { fill: var(--graph-0); }
  text { fill: var(--fg); font-size: 11px; }
</style>
<figure>
  <svg role="img" aria-labelledby="chart-t"
    viewBox="0 0 300 130" width="300">
    <title id="chart-t">Visitors per quarter: Q1 40k,
      Q2 65k, Q3 90k, Q4 110k</title>
    <rect x="10" y="80" width="50" height="40"/>
    <rect x="85" y="55" width="50" height="65"/>
    <rect x="160" y="30" width="50" height="90"/>
    <rect x="235" y="10" width="50" height="110"/>
    <text x="25" y="75">40k</text>
    <text x="250" y="7">110k</text>
  </svg>
  <figcaption>Fig. 1: Visitors nearly tripled over
    the year.</figcaption>
</figure>
Result

Video

<video controls width="1280" height="720"
  poster="/media/tour-poster.jpg" preload="metadata">
  <source src="/media/tour.webm"
    type='video/webm; codecs="vp9, opus"'>
  <source src="/media/tour.mp4"
    type='video/mp4; codecs="avc1.4d401f, mp4a.40.2"'>
  <track kind="captions" src="/media/tour.en.vtt"
    srclang="en" label="English" default>
  <p>Your browser can't play this video.
    <a href="/media/tour.mp4">Download the MP4</a>.</p>
</video>
AttributeValuesNotes
controlsbooleannative, keyboard-accessible controls; leave on unless you build accessible custom ones
autoplaybooleanblocked with sound in all major browsers; see below
mutedbooleanrequired for autoplay
playsinlinebooleaniPhone Safari plays inline instead of going fullscreen
loopbooleanpairs with muted autoplay for ambient clips
posterURLshown before playback; can be the LCP element, so size it like a hero image
preloadnone, metadata, autothe default varies by browser; the spec suggests metadata. none for below-the-fold video
width, heightCSS pxreserve the box, as for img
crossoriginanonymousneeded for cross-origin <track> files and canvas capture
disablepictureinpicture, controlslistChromium-leaning; controlslist="nodownload" is not DRM

Autoplay rules

BrowserAutoplays when
All (Chrome, Firefox, Safari)the video is muted (or has no audio track)
Chromealso with sound after the user has interacted with the site, or once the site's media engagement score is high
Safari on iPhonemuted and playsinline; without playsinline it plays fullscreen. Low Power Mode can still block autoplay
Firefoxper-site setting; muted by default allowed
<!-- ambient background clip: the only reliable autoplay -->
<video autoplay muted loop playsinline
  poster="waves.jpg" width="1920" height="1080">
  <source src="waves.av1.mp4"
    type='video/mp4; codecs="av01.0.05M.08"'>
  <source src="waves.h264.mp4" type="video/mp4">
</video>
  • Anything that moves for more than 5 seconds and starts on its own needs a way to pause it (WCAG 2.2.2); with controls removed, add a pause button. Respect prefers-reduced-motion by not autoplaying at all.
  • Audio that plays automatically for more than 3 seconds needs pause, stop or a separate volume control (WCAG 1.4.2).
  • <source> order is preference order. The first type the browser can play wins, so list AV1 or VP9 before H.264. A type without codecs is only a guess.
  • The codecs string must be quoted inside type; use single quotes around the attribute.

Captions & subtitles: <track> and WebVTT

kindForContains
subtitles (default)viewers who can hear but not understand the languagedialogue translation
captionsdeaf and hard-of-hearing viewersdialogue plus speaker IDs and meaningful sound ("[door slams]")
descriptionsblind viewerstext descriptions of visuals, for synthesis; player support is poor
chaptersnavigationchapter titles
metadatascriptsnot shown
<track kind="captions" src="talk.en.vtt" srclang="en"
  label="English (CC)" default>
<track kind="subtitles" src="talk.es.vtt" srclang="es"
  label="Español">
  • srclang is required when kind="subtitles"; set it on every track anyway.
  • default turns one track on at start; users can change it in the native controls menu.
  • Serve .vtt files as text/vtt. A cross-origin track needs CORS headers and crossorigin on the <video>.
WEBVTT
 
00:00:00.000 --> 00:00:03.500
[upbeat music]
 
00:00:03.500 --> 00:00:07.000
<v Ana>Welcome to the plant tour.
 
00:00:07.000 --> 00:00:11.000 line:10% align:center
<v Ben>Hard hats stay on past this door.
 
NOTE Cue settings: line, position, size, align, vertical

WCAG requires captions for prerecorded video with audio (1.2.2, A), captions for live video (1.2.4, AA) and audio description of prerecorded video (1.2.5, AA). Auto-generated captions count only once someone has corrected them. A transcript on the page helps everyone and is the text alternative for audio-only content (1.2.1).

Audio

<figure>
  <figcaption>Episode 12: Keyboard design</figcaption>
  <audio controls preload="none">
    <source src="ep12.opus" type='audio/ogg; codecs="opus"'>
    <source src="ep12.mp3" type="audio/mpeg">
    <a href="ep12.mp3">Download the episode (MP3)</a>
  </audio>
  <a href="/podcast/12/transcript/">Transcript</a>
</figure>
PointDetail
Attributessame as <video> minus poster, playsinline, width/height
Without controlsthe element is invisible and takes no space
FormatsMP3 and AAC play everywhere; Opus in WebM or Ogg is smaller and widely supported
Accessibilitylabel it (a caption or heading next to it) and link a transcript
Autoplaytreat as blocked; never autoplay audio

For synthesis, analysis and effects use the Web Audio API.

Iframes

<iframe
  src="https://maps.example.com/embed?q=depot"
  title="Map: our depot on Harbor Road"
  width="600" height="400"
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"
  sandbox="allow-scripts allow-same-origin allow-popups"
  allow="fullscreen; geolocation 'none'">
</iframe>
AttributeUse
titlethe accessible name; screen reader users hear it before deciding whether to enter the frame. Required in practice
loading="lazy"defers offscreen embeds; Baseline widely available
sandboxempty = maximum restrictions; add tokens to re-allow capabilities
allowPermissions Policy for the frame: camera, microphone, geolocation, fullscreen, autoplay, payment, clipboard-write…
referrerpolicydefault strict-origin-when-cross-origin; no-referrer hides your URL but breaks embeds that check it
srcdocinline HTML document; overrides src
nametarget for links and forms
allowfullscreenlegacy; prefer allow="fullscreen"
credentiallessload without cookies or storage (Chromium only, limited)

sandbox tokens

TokenRe-allows
(none)nothing: no scripts, forms, popups, top navigation, downloads; unique opaque origin
allow-scriptsJavaScript (but not popups)
allow-same-originkeeps the real origin, so cookies and storage work
allow-formsform submission
allow-popupswindow.open() and target="_blank"
allow-popups-to-escape-sandboxpopups open without inheriting the sandbox (e.g. an ad's landing page)
allow-modalsalert(), confirm(), prompt(), print()
allow-top-navigationnavigating the top page
allow-top-navigation-by-user-activationthe same, only after a click; the safer choice
allow-top-navigation-to-custom-protocolsmailto:, tel: and registered protocols
allow-downloadsdownloads via links
allow-pointer-lock, allow-orientation-lock, allow-presentationthose APIs
allow-storage-access-by-user-activationthe Storage Access API (unpartitioned cookies on request)

srcdoc embeds a whole document inline. Escape its quotes and ampersands in the attribute:

srcdoc.html
<iframe
  title="Sandboxed inline preview"
  sandbox
  srcdoc="<p style='font:14px system-ui'>Rendered
    from <code>srcdoc</code>: no scripts, no forms,
    opaque origin.</p>"
  style="inline-size: 100%; block-size: 80px;
    border: 1px dashed var(--muted);"></iframe>
Result

Responsive frames: set width/height for the ratio, then iframe { inline-size: 100%; block-size: auto; aspect-ratio: 16 / 9; }.

SVG: inline, <img> or CSS

MethodStyleable from page CSSCached separatelyScripts runAccessible nameUse for
Inline <svg>yes (currentColor, custom properties, :hover)no (ships with the HTML)yesrole="img" + <title> / aria-labelicons that follow text color, interactive or animated graphics, charts
<img src="x.svg">noyesnoaltlogos, illustrations, big static drawings
<svg><use href="sprite.svg#id">partly (currentColor and custom properties inherit into the <use> content)yesnoon the outer <svg>many repeated icons; the sprite must be same-origin
CSS background-image / mask-imagecolor via mask + background-coloryesnononedecoration only
<!-- meaningful standalone graphic -->
<svg role="img" aria-labelledby="logo-t" viewBox="0 0 64 16">
  <title id="logo-t">Acme</title>
  <path d="…"/>
</svg>
 
<!-- decorative icon next to visible text -->
<button type="button">
  <svg aria-hidden="true" width="16" height="16">
    <use href="/icons.svg#trash"/>
  </svg>
  Delete
</button>
RuleDetail
Decorative SVGaria-hidden="true"; otherwise some screen readers announce "group" or read stray text
Meaningful SVGrole="img" plus a name; aria-labelledby pointing at the <title> is the most reliable combination
Icon-only controlname the button (aria-label or visually hidden text), hide the SVG
focusable="false"only mattered for IE 11; drop it
fill="currentColor"icons inherit the text color, including in dark mode and forced colors
Untrusted SVGnever inline it: inline SVG runs scripts. <img> is safe

Canvas, <object>, <embed> & image maps

<canvas> is a bitmap: nothing drawn on it is in the accessibility tree. Put fallback content inside the element; it isn't rendered where canvas works, but it can hold real, focusable markup that mirrors the drawing.

<canvas id="sales" width="600" height="300"
  role="img" aria-label="Sales by month, 2026: see table">
  <p>Sales by month: January 12k, February 15k, …</p>
</canvas>
<table><!-- the same data as a table --></table>

For a static chart, role="img" with a summary aria-label plus the data as a table or list is the pragmatic pattern. For an interactive canvas app you must rebuild the semantics yourself; consider SVG or HTML instead. Drawing APIs are in Canvas 2D and WebGL.

<object> and <embed> date from the plugin era (Flash reached end of life in December 2020). Their remaining use is embedding a PDF or an external SVG document.

<object data="/docs/policy.pdf" type="application/pdf"
  width="800" height="600">
  <p><a href="/docs/policy.pdf">Download the policy
    (PDF, 240 KB)</a></p>
</object>
ElementFallback contentVerdict
<object>yes, its childrenacceptable for PDFs; inline PDF display is inconsistent (especially on mobile), so always link the file
<embed>noneavoid
<iframe src="x.pdf">noneworks in desktop browsers with a PDF viewer; same caveat

Image maps make regions of one image into links. The coords are in the image's intrinsic pixels and don't scale with CSS, so they only suit fixed-size images; for anything responsive, use SVG with <a> elements instead.

<img src="floor.png" alt="Floor plan" usemap="#floor"
  width="600" height="400">
<map name="floor">
  <area shape="rect" coords="0,0,300,200"
    href="/rooms/a/" alt="Meeting room A">
  <area shape="circle" coords="450,300,60"
    href="/rooms/pod/" alt="Phone pod">
</map>

Every <area> with an href needs alt.

Performance checklist

LCP
[ ] Identify the LCP element per template (DevTools Performance panel, PageSpeed Insights)
[ ] LCP image is in the HTML (not CSS/JS-injected), or preloaded with <link rel=preload>
[ ] LCP image: no loading="lazy"; fetchpriority="high"
[ ] Served from the same origin or a preconnected CDN
[ ] AVIF/WebP with fallback; compressed; sized by srcset + accurate sizes
 
CLS
[ ] width + height on every img, video, iframe (and on <source> with a different ratio)
[ ] img { max-inline-size: 100%; block-size: auto; }
[ ] Embeds and ads have reserved boxes (aspect-ratio or min-block-size)
[ ] Web fonts don't reflow images (font metrics overrides; see CSS sheet)
 
Bytes
[ ] loading="lazy" on offscreen images and iframes
[ ] preload="none" (or "metadata") on video/audio not seen at load
[ ] No GIF animations: muted looping <video> or animated AVIF/WebP instead
[ ] Largest srcset candidate ≤ 2× the largest slot width
[ ] Heavy third-party embeds (video, maps, social) behind a click-to-load facade
[ ] Long cache lifetimes on hashed media URLs

Common mistakes

MistakeFix
loading="lazy" on the heroremove it; add fetchpriority="high"
No width/heightset them to the intrinsic size; CSS scales
srcset with w descriptors but no sizesthe browser assumes 100vw and downloads too much
sizes copied from a different layoutrecompute from the real CSS; check in DevTools which file loaded (currentSrc)
alt missing or "image"follow the purpose table above
alt on <source>it goes on the <img>
<picture> for plain resolution switching<img srcset sizes> is enough
Unmuted autoplayit won't play; mute it, or wait for a click
Missing playsinlinethe clip goes fullscreen on iPhone
Video without captionsadd a captions track; correct auto-generated ones
<iframe> without titlename it after the content ("Map: …", "Video: …")
sandbox="allow-scripts allow-same-origin" on same-origin untrusted HTMLseparate origin, or drop allow-same-origin
Inline user-uploaded SVGrender it via <img>
Icons from an icon fontinline SVG or <use> sprites: fonts fail, get overridden by user fonts, and read as random characters

Recipes

Responsive hero image with AVIF and WebP

<picture>
  <source type="image/avif" sizes="100vw"
    srcset="hero-800.avif 800w, hero-1200.avif 1200w,
            hero-1600.avif 1600w, hero-2400.avif 2400w">
  <source type="image/webp" sizes="100vw"
    srcset="hero-800.webp 800w, hero-1200.webp 1200w,
            hero-1600.webp 1600w, hero-2400.webp 2400w">
  <img src="hero-1600.jpg" sizes="100vw"
    srcset="hero-800.jpg 800w, hero-1200.jpg 1200w,
            hero-1600.jpg 1600w, hero-2400.jpg 2400w"
    width="2400" height="1200"
    alt="Cyclists crossing the old stone bridge at dawn"
    fetchpriority="high" class="hero">
</picture>
.hero {
  inline-size: 100%;
  block-size: auto;
  max-block-size: 70svh;
  object-fit: cover;
  object-position: 50% 40%;  /* keep the bridge in frame */
}

Lazy thumbnail grid

<ul class="thumbs" role="list">
  <li>
    <a href="/photos/41/">
      <img src="p41-320.jpg" alt="Harbor at low tide"
        srcset="p41-320.jpg 320w, p41-640.jpg 640w"
        sizes="(min-width: 48rem) 12rem, 45vw"
        width="320" height="240" loading="lazy">
    </a>
  </li>
  <!-- … -->
</ul>
.thumbs {
  display: grid;
  grid-template-columns:
    repeat(auto-fill, minmax(10rem, 1fr));
  gap: 0.5rem;
  padding: 0;
}
.thumbs img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

The first row is usually visible at load: leave loading="lazy" off those few, or accept the small delay if the grid is below the fold.

Captioned video

<figure class="video">
  <video controls preload="metadata" playsinline
    width="1280" height="720" poster="demo-poster.jpg">
    <source src="demo.webm" type="video/webm">
    <source src="demo.mp4" type="video/mp4">
    <track kind="captions" src="demo.en.vtt"
      srclang="en" label="English (CC)" default>
    <track kind="subtitles" src="demo.fr.vtt"
      srclang="fr" label="Français">
    <a href="demo.mp4">Download the demo video</a>
  </video>
  <figcaption>Setting up the sensor in 90 seconds.
    <a href="demo-transcript/">Read the transcript</a>.
  </figcaption>
</figure>
.video video {
  inline-size: 100%;
  block-size: auto;
}
::cue {
  font-size: 1.1em;
  background: rgb(0 0 0 / 80%);  /* legible over any frame */
}

Privacy-friendly YouTube embed

youtube-nocookie.com is YouTube's privacy-enhanced mode: YouTube says views in it are not used to personalize the viewer's YouTube experience or ads outside your site. It is not zero tracking, and the player still loads hundreds of kilobytes of script, so load it lazily. YouTube's API terms require the embed to send a Referer, so keep the default policy rather than no-referrer, and keep the player at least 200 × 200 px.

<iframe
  class="yt"
  src="https://www.youtube-nocookie.com/embed/VIDEO_ID"
  title="Video: How our heat pump works (4 min)"
  width="560" height="315"
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"
  allow="encrypted-media; picture-in-picture; fullscreen">
</iframe>
.yt {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 16 / 9;
  border: 0;
}

The stricter option is a facade: render the poster (https://i.ytimg.com/vi/VIDEO_ID/hqdefault.jpg) inside a link to the video, and swap in the iframe with a few lines of JS only when the user clicks. Nothing loads from YouTube until then. Under GDPR-style consent regimes the facade is the usual way to avoid loading third-party content before consent.

Accessible inline SVG icon button

The button carries the name; the SVG is hidden and inherits the text color:

icon-button.html
<style>
  .icon-btn { display: inline-grid; place-items: center;
    inline-size: 2.75rem; block-size: 2.75rem;
    border: 1px solid var(--muted); border-radius: 8px;
    background: none; color: var(--fg); cursor: pointer; }
  .icon-btn:hover { color: var(--graph-1); }
  .icon-btn:focus-visible {
    outline: 2px solid var(--graph-0); outline-offset: 2px; }
</style>
<button type="button" class="icon-btn" aria-label="Delete">
  <svg aria-hidden="true" viewBox="0 0 24 24" width="20"
    height="20" fill="none" stroke="currentColor"
    stroke-width="2" stroke-linecap="round">
    <path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13
      M9 7V4h6v3"/>
  </svg>
</button>
Result

Prefer visible text where space allows; for icon-only buttons add a tooltip that shows the same word on hover and focus, so the visible and accessible names match (WCAG 2.5.3).

References