Picture
<cw-picture> | CwPicture
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' }} /> );
width and height describe the image’s intrinsic size so the browser can work out
its aspect ratio — they don’t set the rendered size. Size the element itself with CSS; the image scales to
fill it.
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> );
Only <source> elements that are direct children are used — any other slotted element is
ignored. <cw-picture> watches its <source> children, so adding,
removing, or changing one updates the image, but it renders its own <img>; a slotted
<img> has no effect.
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.
<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.
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.