fdb/docs

[§] /docs/rules/media

Media

AVIF first, real sizes, a reserved box for every image and video, and alt text that says what the picture is for.

TL;DR

  • Photos in AVIF with WebP and JPEG fallbacks. Logos in SVG. Motion in MP4, never GIF.
  • Set sizes on every responsive image.
  • Reserve the box: width and height, or aspect-ratio.
  • Lazy-load below the fold, never the LCP image.
  • Alt text says what the picture is for. Empty alt for decoration.

Images and video are usually the heaviest thing on a page and the most common cause of layout shift. The conventions below fix both. Serve modern formats, send the size the screen needs, reserve the space before the file arrives, and describe the picture for people who cannot see it.

Pick the format

Which format?

if it is a photo or a rendered scene

→ AVIF, with WebP and JPEG fallbacks

if it is a logo, icon or line diagram

→ SVG

if it is a screenshot with text or hard edges

→ WebP lossless or PNG

if it moves

→ MP4 (H.264) video, never GIF

Choose by content, not habit. Most sites only ever need these four answers.
Pick the format: table (6 rows)
FormatUse forQuality settingNotes
AVIFPhotos, gradients, hero art50 to 60Smallest files. Slower to encode. Supported by all current major browsers.
WebPFallback for AVIF, screenshots75 to 80 lossy, or losslessGood middle ground.
JPEGLast-resort fallback, email75 to 82, progressiveEverything opens it.
PNGScreenshots that need exact pixelslosslessRun it through oxipng or similar.
SVGLogos, icons, diagramsn/aOptimise with SVGO. Set viewBox, drop fixed width and height.
GIFNothingn/aConvert to MP4. It is many times larger for the same clip.
  • MUST NOT ship a photo as PNG or an animation as GIF.
  • MUST strip EXIF and location metadata from photos before publishing.
  • SHOULD let the framework convert formats. next/image serves AVIF or WebP when you set images.formats in next.config.
// next.config.mjs
export default {
  images: { formats: ['image/avif', 'image/webp'] },
};

Weight targets

Weight targets: table (5 rows)
AssetTargetHard ceiling
Hero or LCP image150 KB250 KB
Content image in the flow80 KB150 KB
Thumbnail or avatar15 KB30 KB
Background video loop (10 s or less)1.5 MB3 MB
OG image150 KB300 KB

These are this guide's budgets, not a standard. Pick your own, write them down, and check them in review.

Send the right size

A 2400px photo on a 390px phone wastes most of its bytes. Give the browser a list of widths and tell it how wide the image will be drawn.

Send the right size: copy the html (8 lines)
<img
  src="/img/hero-1200.jpg"
  srcset="/img/hero-640.avif 640w, /img/hero-1200.avif 1200w, /img/hero-2000.avif 2000w"
  sizes="(min-width: 1024px) 50vw, 100vw"
  width="1200"
  height="800"
  alt="Two riso inks mixing in water, orange over blue"
/>
Send the right size: copy the tsx (8 lines)
import Image from 'next/image';

<Image
  src={hero}
  alt="Two riso inks mixing in water, orange over blue"
  sizes="(min-width: 1024px) 50vw, 100vw"
  preload            // only for the LCP image (was `priority` before Next 16)
/>
  • MUST set sizes on every responsive image. Without it the browser assumes 100vw and downloads the largest file.
  • MUST generate widths up to 2× the largest drawn size, and no further.
  • MUST use loading="lazy" on images below the fold, and MUST NOT lazy-load the LCP image.
  • SHOULD use widths from one shared list, for example 640, 750, 828, 1080, 1200, 1920, 2048. These are the next/image defaults.

Reserve the box

16:9 video, hero

1.91:1 OG card

3:2 photo

4:5 portrait

1:1 avatar, tile

The ratios you will actually use. Give every media slot one of these before the file loads, and nothing shifts when it arrives.
Reserve the box: copy the css (12 lines)
.media {
  aspect-ratio: 16 / 9;
  width: 100%;
  overflow: hidden;
  background: var(--surface-raised); /* the placeholder colour */
}
.media > img,
.media > video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
  • MUST give every img and video either width and height attributes or a CSS aspect-ratio.
  • SHOULD use a flat placeholder colour or a tiny blurred preview, never a spinner.
  • SHOULD crop with object-fit: cover and set object-position when the subject is off-centre.

Video

Video: copy the html (14 lines)
<video
  autoplay
  muted
  loop
  playsinline
  preload="metadata"
  poster="/video/loop-poster.avif"
  width="1920"
  height="1080"
  aria-hidden="true"
>
  <source src="/video/loop.webm" type="video/webm" />
  <source src="/video/loop.mp4" type="video/mp4" />
</video>
Video: table (5 rows)
AttributeWhy
mutedBrowsers only allow autoplay without sound.
playsinlineStops iOS Safari from opening the video full screen.
posterShows at once, and is what reduced-motion users see.
preload="metadata"Fetches size and duration, not the whole file.
aria-hidden="true"Only for purely decorative loops. Remove it for content video.
  • MUST strip the audio track from any video that autoplays muted. It is dead weight.
  • MUST show a visible pause control on any decorative loop longer than 5 seconds (WCAG 2.2.2).
  • MUST show the poster and no autoplay under prefers-reduced-motion: reduce.
  • MUST caption any video with speech (WCAG 1.2.2) and provide a transcript for audio-only content.
  • SHOULD encode loops at 1080p or less, 24 to 30 fps, H.264 MP4 plus an optional WebM (VP9 or AV1) listed first.
  • SHOULD host long video on a streaming service or use HLS. A 2-minute MP4 in public/ is a bandwidth bill.
# A web-ready silent loop: H.264, no audio, streamable
ffmpeg -i in.mov -an -vf "scale=1920:-2,fps=30" -c:v libx264 -crf 26 -preset slow \
  -pix_fmt yuv420p -movflags +faststart loop.mp4

Alt text

Alt text: table (6 rows)
ImageAltRule
Decorative texturealt=""Empty, not missing. Screen readers skip it.
Product photoalt="Walnut desk, 140 cm wide, with two drawers"Say what matters for the decision.
Chartalt="Sign-ups doubled from March to June"Give the takeaway, and put the data in a table nearby.
Logo as a linkalt="Acme home"Describe where the link goes.
Screenshot of textthe text itself, or a summaryBetter still, use real text.
Icon buttonno image alt; aria-label="Close" on the buttonLabel the control, not the drawing.
  • MUST give every img an alt attribute. Empty for decoration, meaningful for content.
  • MUST NOT start with "Image of" or "Picture of". Screen readers already say "image".
  • MUST NOT repeat the caption or the nearby text word for word.
  • SHOULD keep alt to one sentence. Longer descriptions go in the page or a figcaption.

Social cards and icons

Social cards and icons: table (6 rows)
FileSizeNext.js file convention
Open Graph image1200 × 630 px (1.91:1)app/opengraph-image.png or .tsx
X (Twitter) card1200 × 630 px, summary_large_imageapp/twitter-image.png, or reuse the OG image
Favicon (legacy)32 × 32 px .ico, with 16 × 16 insideapp/favicon.ico
Favicon (modern)SVG, with its own dark-mode @media ruleapp/icon.svg
Apple touch icon180 × 180 px PNG, no transparencyapp/apple-icon.png
Web app manifest icons192 × 192 and 512 × 512 px PNG, plus a 512 maskableapp/manifest.ts
  • SHOULD keep the OG image's text away from the outer 60px or so. Some platforms crop or round the edges.
  • MUST set og:image:alt when the card carries information.
  • SHOULD check the favicon at 16px. If it does not read, simplify it to one shape or one letter.

Why it works

Format and size rules remove most of a page's weight without anyone seeing a difference. Reserved boxes mean nothing jumps as files arrive, which is what CLS measures. Alt text rules turn "add some alt" into a checkable habit, and fixed card and icon sizes mean a link looks right wherever it is shared.

Sources

3 sources

On this page