Skip to content

Rules

A rule is a matcher plus what to generate from the images it matches. The same rule objects go into the Vite plugin, the loader, the cli config and an import query, so this page applies to all of them.

rules: [
// Animations: gif for the reach, webp for the size - both keep the frames.
{
match: '**/*.gif',
format: ['gif', 'webp']
},
// Stickers and screenshots: png keeps the transparency.
{
match: '**/*.png',
width: [1, 0.5],
format: ['png', 'webp']
},
// Everything else.
{
width: [1920, 1280, 640],
format: ['jpg', 'webp', 'avif']
}
]
FieldTypeDefaultWhat it does
matchMatcher | Matcher[]matches everythingGlob, media query or function to match the image by.
fallthroughbooleanfalseKeep matching the rest of the rules after this one matched.
widthnumber | number[]source widthWidths to resize to. A value of 1 or less is a multiplier.
formatImageFormat | ImageFormat[]source formatFormats to convert to: avif, webp, jpg, png, gif, svg.
processingProcessingOptionsjpg.mozjpeg, png.palettesharp output options per format.
optimizationOptimizationOptionsnoneCustom optimizer function per format.
skipOptimizationbooleanfalseDo not re-encode the original variant, and skip the optimizers.
scalingUpbooleantrueEmit variants requested wider than the source, capped at the source width.
postfixstring | PostfixFormatter@<width>wPostfix added to the variant file name.

match, fallthrough, width and format only exist on a rule. The encoder options below them - processing, optimization, skipOptimization, scalingUp and postfix - can also be set once on the integration itself, where they become the default for every rule, and a rule that sets the same option overrides it - processing and optimization merge with the integration-level ones per format instead of replacing them. concurrency and the disk cache are integration-level only.

Rules are tried in order and matching stops at the first hit: an image is generated by exactly one rule, unless that rule sets fallthrough: true and lets the search continue.

rules: [
// Hero images get an extra large variant, and the general rule after it.
{
match: '**/hero.*',
width: [2560],
format: ['jpg', 'webp'],
fallthrough: true
},
{
width: [1, 0.5],
format: ['jpg', 'webp', 'avif']
}
]

Two rules that resolve to one and the same file produce it once - the first rule wins, as it does without fallthrough.

A rule without match matches everything, which makes it the catch-all, and a catch-all belongs last: rules after it are unreachable.

match takes a glob, a media query, a function, or an array of them.

A picomatch pattern, tested against the absolute path of the source file with posix separators:

{
match: '**/*.{jpg,jpeg}'
}

Anchor patterns with **/, always. 'src/images/*.jpg' matches nothing, because the path it is tested against starts at the root of the file system; '**/src/images/*.jpg' is the pattern that works.

A string with a width or height feature is read as a CSS media query and matched against the size of the source image, not against a viewport:

{
match: '(min-width: 1920px)',
width: [1, 0.5, 0.25]
}

A comma separated list is an or, exactly as in css: '(min-width: 1920px), (min-height: 1920px)' matches an image that is large in either direction. Only width and height features turn a string into a media query - a parenthesised extglob is still a glob.

{
// Panoramas only.
match: (path, size) => size.width / size.height > 2,
width: [1, 0.5],
format: ['jpg', 'webp']
}

The matcher gets the source file path as the integration feeds it, the size in pixels as { width, height }, and the source file itself as { path, contents }.

{
// Big jpgs, and nothing else.
match: ['**/*.jpg', '(min-width: 1920px)']
}

Every matcher in the array has to match. For an or, write separate rules, one brace glob, or one comma separated media query list.

width takes one number or a list of them:

  • a value of 1 or less is a multiplier of the source width - [1, 0.5] is “the original and a half sized copy”, and the multiplied width is rounded up;
  • a value above 1 is absolute pixels, and has to be an integer - 1.5 fails the build with Invalid width: 1.5;
  • with no width at all only the source width is generated.

Pixels are never upscaled. A width above the source width still produces a variant, capped at the source width and named after the width it actually has: a 1600px source asked for 2400 yields photo@1600w.jpg. Set scalingUp: false to drop those variants instead of capping them - useful when the same rule set covers sources of very different sizes.

Requested widths that resolve to one and the same file are generated once, so an oversized list costs nothing beyond the encoding it does need.

The multiplier 1 is the one width that gets no postfix: it keeps the source file name, photo.jpg and photo.webp. An absolute width equal to the source width still gets one, photo@1600w.jpg.

format takes one format or a list: avif, webp, jpg, png, gif, svg. jpeg and heif are accepted as aliases of jpg and avif; anything else fails the build with Unsupported image format: "tiff". With no format the source format is kept.

Put the widely supported format first and the modern ones after it - ['jpg', 'webp', 'avif']. The order only decides which variant is the fallback when the source format is not in the list, and that fallback is what a browser gets when it supports nothing else.

Give these their own rules, before the catch-all:

  • png converted to jpg loses its transparency. Keep png in the list: format: ['png', 'webp'].
  • animated gif keeps its frames only as gif or webp. Jpg and avif flatten it to a single frame.
  • svg is never resized and never converted. A rule passes it through only when its format is unset or includes svg, so a raster-only format drops it silently. The Vite plugin skips .svg imports entirely, and the loader should keep .svg out of its test; the cli does pass svg files through.
{
processing: {
jpg: {
quality: 80
},
avif: {
effort: 6
}
},
optimization: {
// `optimize` is svgo's.
svg: contents => Buffer.from(optimize(contents.toString()).data)
},
postfix: (width, requestedWidth, format) => `-${width}`
}
  • processing - sharp output options per format, merged per format over the defaults jpg: { mozjpeg: true } and png: { palette: true }. Overriding jpg.quality therefore keeps mozjpeg on. The defaults are chosen by SSIM and file size: mozjpeg gives around 30% smaller jpg files at the same quality, palette turns on lossy quantization for png.
  • optimization - a (contents, format) => Buffer function per format, applied to the already encoded bytes. It is the only way to touch svg, e.g. with svgo.
  • skipOptimization - the variant that matches the source format at the source width is passed through byte for byte instead of being re-encoded, and the custom optimizers never run. Resized and converted variants are still encoded.
  • postfix - a string, or a (width, requestedWidth, format) => string formatter. width is the actual output width, requestedWidth is what the rule asked for - the multiplier, when it was one. The formatter has to be pure: it is called repeatedly, including for variants that end up skipped, and its result is part of the cache key.
WhereHow
Vite pluginrules in the plugin options.
Loaderrules in the loader options.
Clirules in srcset.config.js. The -m, -w and -f arguments build one rule that replaces the whole list.
Import queryA JSON rule in the query replaces the whole rule set for that one import.
  • No catch-all, no image. A rule set that misses an image leaves an empty module behind, and nothing on the page says so.
  • An array in match is an and. ['**/*.jpg', '**/hero*'] matches only the jpgs whose name starts with hero.
  • Globs are absolute. Without a leading **/ a path pattern matches nothing.
  • The first format is the fallback only when the source format is missing from the list. ['avif', 'jpg'] on a jpg source still hands out the jpg; on a png source it hands avif to everyone.
  • Png and gif in the catch-all. A general format: ['jpg', 'webp', 'avif'] strips transparency from png files and flattens animated gifs. They need rules of their own.
  • Svg in a raster rule. A format without svg drops svg files without a word.