Skip to content

Runtime

@srcset/runtime is the only part of srcset that reaches the browser: it turns generated variants into DOM attributes. Zero dependencies, tree-shakable, and the whole public surface is tested against a 1 kB budget.

Everything else in the project speaks its language: the image modules of the bundler integrations, the baked modules of the cli and the proxy adapters all produce arrays of SrcSetEntry, and the framework components are built on the two functions below.

Install it as a regular dependency, not a dev one - it ships:

pnpm add @srcset/runtime

One generated image variant:

FieldTypeDescription
idstringId of the variant: the format plus the width by default, e.g. webp600. The key under which the variant sits in srcMap; a build-time integration can change the scheme with resourceId.
formatImageFormatavif, webp, jpg, png, gif or svg.
typestringMime type of the variant, e.g. image/webp. What the <source> grouping runs on.
widthnumberWidth in pixels. Becomes the w descriptor.
heightnumber | undefinedHeight in pixels. Optional: a build-time variant knows it, a runtime-built one - an imgproxy or Cloudflare url - does not.
urlstringUrl of the variant.
import type { SrcSetEntry } from '@srcset/runtime'

The srcSet export of an image module is a SrcSetEntry[], and its src export is the one entry the module selected for the <img> - by default the variant matching the source format and width, or the first generated one when there is no such variant.

getImageProps(
src: SrcSetEntry | undefined,
srcSet: SrcSetEntry[] | string | undefined
): ImageSrcProps

Builds the two attributes of an <img> - the exported ImageSrcProps is { src?: string, srcSet?: string }, both optional:

import { getImageProps } from '@srcset/runtime'
import { src, srcSet } from './photo.jpg'
getImageProps(src, srcSet)
// {
// src: '/photo.jpg',
// srcSet: '/photo.jpg 1200w, /photo@600w.jpg 600w'
// }

The srcSet string holds only the variants of the src format. A single <img> gets one srcset attribute, and a browser picks a candidate from it by width alone - mixing formats in there would hand avif to a browser that cannot decode it. Formats belong in <source> elements, which is what getSourceProps is for.

The rest of the behaviour:

  • A srcSet that would duplicate src is omitted. With one variant of the src format there is nothing to choose from, so the result is just { src } and the markup stays clean.
  • A ready string passes through. getImageProps(src, '/photo.jpg 1x, /photo@2x.jpg 2x') returns it untouched as the srcSet, next to the src url, which is how a component accepts both an array and a hand-written srcset.
  • Without src the whole set becomes the srcSet string, formats included - for a <source>-like case where the caller has already narrowed the set.
  • Without either the result is an empty object, so nothing is rendered rather than an src="" that requests the current page.
getSourceProps(srcSet: SrcSetEntry[]): { type: string, srcSet: string }[]

Builds the <source> elements of a <picture>, one per mime type:

getSourceProps(srcSet)
// [
// { type: 'image/avif', srcSet: '/photo.avif 1200w, /photo@600w.avif 600w' },
// { type: 'image/webp', srcSet: '/photo.webp 1200w, /photo@600w.webp 600w' },
// { type: 'image/jpeg', srcSet: '/photo.jpg 1200w, /photo@600w.jpg 600w' }
// ]

Two things happen there, and both matter.

Grouping by type. A browser picks the first <source> whose type it can decode, then chooses a candidate from that source’s srcset by width. So one <source> per format, each carrying every width of that format.

Ordering by format efficiency. Avif first, then webp, then every other mime type in the order it appeared in the array. The browser takes the first source it can decode and never looks further, so the order in the markup is the preference: a browser without avif falls to webp, one without either falls through to the <img>. The format order of the generation rule does not carry over here - avif and webp are hoisted to the front wherever they sat in it.

Put together, by hand:

Photo.jsx
import { getImageProps, getSourceProps } from '@srcset/runtime'
import { src, srcSet } from './photo.jpg'
const sizes = '(max-width: 48rem) 100vw, 48rem'
export function Photo() {
return (
<picture>
{getSourceProps(srcSet).map(source => (
<source
key={source.type}
{...source}
sizes={sizes}
/>
))}
<img
{...getImageProps(src, srcSet)}
sizes={sizes}
alt='A photo'
/>
</picture>
)
}
toSrcSetString(srcSet: SrcSetEntry[]): string
toDensitySrcSetString(srcSet: SrcSetEntry[]): string

toSrcSetString builds the srcset value with width descriptors - url 1200w, url 600w - which is what both functions above use. A set of one returns the bare url with no descriptor: a single candidate is not a choice, and a lone w descriptor would drag sizes along with it.

toDensitySrcSetString builds it with pixel density descriptors instead, computed from the variant widths relative to the smallest one and rounded to two decimals:

import { filterBy, toDensitySrcSetString } from '@srcset/runtime'
toDensitySrcSetString(filterBy(srcSet, 'format', 'jpg'))
// '/photo.jpg 2x, /photo@600w.jpg 1x'

Use it for images of a fixed display size - a logo, an avatar, an icon - where the layout width is known and width descriptors would need a sizes attribute to say what it is. Pass one format at a time, the same way getImageProps does.

FunctionDescription
groupBy(srcSet, field)Group entries by a field into [value, entries] pairs, keeping the original order inside every group and between the groups.
filterBy(srcSet, field, value)Entries whose field equals the value, e.g. one format out of a mixed set.

Both are typed against SrcSetEntry, so field autocompletes and value is checked against that field’s type.

mimeTypes maps a format to its mime type - avif, webp, jpg, png, gif, svg to image/avif, image/webp, image/jpeg, image/png, image/gif, image/svg+xml - and ImageFormat is the union of those six names. They are the browser-side copies of the ones in @srcset/core, so nothing pulls sharp into a bundle.