Skip to main content
Light Dark System

Picture

<cw-picture> | CwPicture
Since 1.6 experimental

A responsive image with resolution switching, art direction, format negotiation, and layout-shift-free loading, wrapping the native <picture> element.

Getting a responsive image right by hand is surprisingly easy to get wrong. <cw-picture> wraps the browser’s native <picture> and srcset machinery, so the browser still does the choosing — and adds the pieces around it that are easy to forget: reserved layout space, a placeholder, and an explicit way to say an image is decorative.

Start with src and alt. Set width and height to the image’s intrinsic size so the browser can reserve space for it before it loads.

<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
  alt="A valley at sunrise, ringed by granite cliffs"
  width="800"
  height="533"
  style="max-width: 400px;"
></cw-picture>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <CwPicture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
    alt="A valley at sunrise, ringed by granite cliffs"
    width={800}
    height={533}
    style={{ maxWidth: '400px' }}
  />
);

Examples

Alternative Text

Every meaningful image needs alt text describing what it shows. If an image is purely decorative and conveys nothing a screen reader user would miss, add decorative instead — it renders alt="", so assistive devices skip the image entirely. Setting alt="" yourself does the same thing.

If you set neither, <cw-picture> renders an empty alt and logs a warning to the console, so a forgotten description never passes silently as an intentional choice.

<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
  width="800"
  height="533"
  decorative
  style="max-width: 400px;"
></cw-picture>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <CwPicture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
    width={800}
    height={533}
    decorative
    style={{ maxWidth: '400px' }}
  />
);

Multiple Resolutions

Use srcset with density descriptors (1x, 2x, 3x) to serve a sharper image to high-density screens. The browser picks the candidate that matches the device’s pixel ratio.

<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"
  srcset="
    https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80 1x,
    https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80 2x,
    https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1200&q=80 3x
  "
  alt="A valley at sunrise, ringed by granite cliffs"
  width="400"
  height="267"
  style="max-width: 400px;"
></cw-picture>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <CwPicture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80"
    srcset="
      https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80 1x,
      https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80 2x,
      https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1200&q=80 3x
    "
    alt="A valley at sunrise, ringed by granite cliffs"
    width={400}
    height={267}
    style={{ maxWidth: '400px' }}
  />
);

Multiple Sizes

When an image’s rendered size changes with the viewport, use srcset with width descriptors (400w, 800w) plus sizes, which tells the browser how wide the image will be. That lets it choose a file before layout, rather than after — which is why sizes matters as much as srcset does.

Resize your window to see the browser switch candidates; the image below fills the full width of its container on small screens and half of it on larger ones.

<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
  srcset="
    https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80 400w,
    https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80 800w,
    https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1600&q=80 1600w
  "
  sizes="(min-width: 800px) 50vw, 100vw"
  alt="A valley at sunrise, ringed by granite cliffs"
  width="800"
  height="533"
></cw-picture>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <CwPicture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
    srcset="
      https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&q=80 400w,
      https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80 800w,
      https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1600&q=80 1600w
    "
    sizes="(min-width: 800px) 50vw, 100vw"
    alt="A valley at sunrise, ringed by granite cliffs"
    width={800}
    height={533}
  />
);

Art Direction

Sometimes a smaller screen needs a different crop, not just a smaller copy of the same image. Slot <source> elements with a media condition, exactly as you would inside a native <picture>. The first one whose condition matches wins; src is the fallback.

Resize your window below 600px to switch to the square crop.

<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1200&h=500&fit=crop&q=80"
  alt="A valley at sunrise, ringed by granite cliffs"
  width="1200"
  height="500"
>
  <source
    media="(max-width: 600px)"
    srcset="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600&h=600&fit=crop&q=80"
  />
</cw-picture>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <CwPicture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=1200&h=500&fit=crop&q=80"
    alt="A valley at sunrise, ringed by granite cliffs"
    width={1200}
    height={500}
  >
    <source
      media="(max-width: 600px)"
      srcSet="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600&h=600&fit=crop&q=80"
    />
  </CwPicture>
);

Format Negotiation

Slot <source> elements with a type so the browser can pick a modern format like AVIF or WebP when it supports one, and fall back to src (typically a JPEG or PNG) when it doesn’t. Order them from most to least preferred.

<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&fm=jpg&q=80"
  alt="A valley at sunrise, ringed by granite cliffs"
  width="800"
  height="533"
  style="max-width: 400px;"
>
  <source type="image/avif" srcset="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&fm=avif&q=80" />
  <source type="image/webp" srcset="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&fm=webp&q=80" />
</cw-picture>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <CwPicture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&fm=jpg&q=80"
    alt="A valley at sunrise, ringed by granite cliffs"
    width={800}
    height={533}
    style={{ maxWidth: '400px' }}
  >
    <source type="image/avif" srcSet="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&fm=avif&q=80" />
    <source type="image/webp" srcSet="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&fm=webp&q=80" />
  </CwPicture>
);

Aspect Ratio

By default, the aspect ratio comes from width and height, so the space is reserved before the image loads and nothing shifts when it arrives. Set aspect-ratio (any valid CSS aspect-ratio value) to override it and crop every image to a consistent shape instead. Use fit and position to control how the image fills that shape.

<div style="display: flex; gap: 1rem;">
  <cw-picture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
    alt="A valley at sunrise, ringed by granite cliffs"
    aspect-ratio="1 / 1"
    style="flex: 1;"
  ></cw-picture>
  <cw-picture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
    alt="A valley at sunrise, ringed by granite cliffs"
    aspect-ratio="1 / 1"
    fit="contain"
    style="flex: 1;"
  ></cw-picture>
  <cw-picture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
    alt="A valley at sunrise, ringed by granite cliffs"
    aspect-ratio="1 / 1"
    position="left"
    style="flex: 1;"
  ></cw-picture>
</div>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <div style={{ display: 'flex', gap: '1rem' }}>
    <CwPicture
      src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
      alt="A valley at sunrise, ringed by granite cliffs"
      aspectRatio="1 / 1"
      style={{ flex: 1 }}
    />
    <CwPicture
      src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
      alt="A valley at sunrise, ringed by granite cliffs"
      aspectRatio="1 / 1"
      fit="contain"
      style={{ flex: 1 }}
    />
    <CwPicture
      src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
      alt="A valley at sunrise, ringed by granite cliffs"
      aspectRatio="1 / 1"
      position="left"
      style={{ flex: 1 }}
    />
  </div>
);

Lazy Loading

Images load lazily by default, so ones far below the fold cost nothing until someone scrolls near them. For an image that’s visible on first paint — a hero image, say — set loading="eager" and fetchpriority="high" so the browser fetches it as early as possible.

<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
  alt="A valley at sunrise, ringed by granite cliffs"
  width="800"
  height="533"
  loading="eager"
  fetchpriority="high"
  style="max-width: 400px;"
></cw-picture>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <CwPicture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
    alt="A valley at sunrise, ringed by granite cliffs"
    width={800}
    height={533}
    loading="eager"
    fetchpriority="high"
    style={{ maxWidth: '400px' }}
  />
);

Placeholder

Until the image loads, <cw-picture> shows a solid color — set it with the --placeholder-color custom property. For something closer to the real image, pass a tiny, low-quality version as placeholder (a URL or a data URI); it’s shown blurred, then fades out as the real image arrives.


<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
  placeholder="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=24&q=20"
  alt="A valley at sunrise, ringed by granite cliffs"
  width="800"
  height="533"
  style="max-width: 400px;"
></cw-picture>
<br />
<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
  alt="A valley at sunrise, ringed by granite cliffs"
  width="800"
  height="533"
  style="max-width: 400px; --placeholder-color: var(--cw-color-primary-200);"
></cw-picture>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <>
    <CwPicture
      src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
      placeholder="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=24&q=20"
      alt="A valley at sunrise, ringed by granite cliffs"
      width={800}
      height={533}
      style={{ maxWidth: '400px' }}
    />
    <br />
    <CwPicture
      src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
      alt="A valley at sunrise, ringed by granite cliffs"
      width={800}
      height={533}
      style={{ maxWidth: '400px', '--placeholder-color': 'var(--cw-color-primary-200)' }}
    />
  </>
);

Captions

Use the caption slot to add a caption below the image. The picture is then exposed to assistive devices as a figure, named by its caption.

Sunrise over the valley floor, shot from the eastern rim.
<cw-picture
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
  alt="A valley at sunrise, ringed by granite cliffs"
  width="800"
  height="533"
  style="max-width: 400px;"
>
  <span slot="caption">Sunrise over the valley floor, shot from the eastern rim.</span>
</cw-picture>
import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

const App = () => (
  <CwPicture
    src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
    alt="A valley at sunrise, ringed by granite cliffs"
    width={800}
    height={533}
    style={{ maxWidth: '400px' }}
  >
    <span slot="caption">Sunrise over the valley floor, shot from the eastern rim.</span>
  </CwPicture>
);

Load and Error Events

<cw-picture> emits cw-load once the image loads and cw-error if it can’t. It also reflects loaded and error attributes, so both states are stylable and inspectable. A cw-error doesn’t replace the image with any built-in fallback — the placeholder simply stays visible; listen for the event if you’d like to swap in something else.

<cw-picture
  id="events-example"
  src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=800&q=80"
  alt="A valley at sunrise, ringed by granite cliffs"
  width="800"
  height="533"
  loading="eager"
  style="max-width: 400px;"
></cw-picture>

<script>
  const picture = document.getElementById('events-example');
  picture.addEventListener('cw-load', () => console.log('Loaded!'));
  picture.addEventListener('cw-error', () => console.log('Failed to load.'));
</script>

Importing

If you’re using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use any of the following snippets to cherry pick this component.

Script Import Bundler React

To import this component from the CDN using a script tag:

<script type="module" src="https://cdn.jsdelivr.net/npm/@cordwainer/cw-elements@1.6.0/cdn/components/picture/picture.js"></script>

To import this component from the CDN using a JavaScript import:

import 'https://cdn.jsdelivr.net/npm/@cordwainer/cw-elements@1.6.0/cdn/components/picture/picture.js';

To import this component using a bundler:

import '@cordwainer/cw-elements/dist/components/picture/picture.js';

To import this component as a React component:

import CwPicture from '@cordwainer/cw-elements/dist/react/picture';

Slots

Name Description
(default) Optional <source> elements, used exactly like the ones inside a native <picture> — media for art direction, type for format negotiation, srcset/sizes for resolution switching. Any other slotted element is ignored.
caption Optional caption shown below the image.

Learn more about using slots.

Properties

Name Description Reflects Type Default
src The URL of the fallback image, used when no srcset or slotted <source> matches. string | undefined -
srcset One or more candidate image URLs with either density (1x, 2x) or width (400w, 800w) descriptors. Use width descriptors together with sizes so the browser can pick an image before layout. string | undefined -
sizes The rendered width of the image at different viewport conditions. Only meaningful with width descriptors in srcset. string | undefined -
alt A description of the image for assistive devices. Required for meaningful images. An empty string (alt="") marks the image as decorative, the same as the decorative attribute does. string | undefined -
decorative Marks the image as purely decorative, so assistive devices skip it. Renders alt="", ignoring alt. boolean false
width The image’s intrinsic width in pixels. Together with height, this reserves the right amount of layout space before the image loads. It does not set the rendered size — size the element with CSS. number | undefined -
height The image’s intrinsic height in pixels. See width. number | undefined -
aspectRatio
aspect-ratio
Overrides the aspect ratio implied by width and height, as a valid CSS aspect-ratio value (e.g. "16 / 9"). Useful to crop every image to a consistent shape — see fit and position. string ''
loading Indicates how the browser should load the image. Use eager (with fetchpriority="high") for images that are visible on first paint, such as a hero image. 'eager' | 'lazy' 'lazy'
decoding Hints how the browser should decode the image. 'sync' | 'async' | 'auto' 'auto'
fetchpriority Hints at the relative priority the browser should give this image when fetching it. 'high' | 'low' | 'auto' 'auto'
crossorigin Sets the CORS mode used when fetching the image. 'anonymous' | 'use-credentials' | undefined -
referrerpolicy Controls how much referrer information is sent when fetching the image. ReferrerPolicy | undefined -
fit How the image is resized to fit its frame. Only visible when aspect-ratio crops the image. 'contain' | 'cover' | 'fill' | 'none' | 'scale-down' 'cover'
position How the image is positioned within its frame, as a valid CSS object-position value. string 'center'
placeholder The URL (or data URI) of a low-quality image to show, blurred, until the real image loads. Without it, a solid --placeholder-color is shown instead. string | undefined -
loaded Indicates whether the image has loaded successfully. boolean false
error Indicates whether the image failed to load. boolean false
updateComplete A read-only promise that resolves when the component has finished updating.

Learn more about attributes and properties.

Events

Name React Event Description Event Detail
cw-load onCwLoad Emitted when the image loads successfully. -
cw-error onCwError Emitted when the image fails to load. -

Learn more about events.

Custom Properties

Name Description Default
--placeholder-color The solid color shown until the image loads. var(–cw-color-neutral-100)

Learn more about customizing CSS custom properties.

Parts

Name Description
base The component’s base wrapper.
frame The container that holds the placeholder and the image, and reserves the image’s layout space.
placeholder The placeholder shown until the image loads.
image The underlying <img> element.
caption The container wrapping the caption slot.

Learn more about customizing CSS parts.