Responsive Images & Media
Combine responsive properties, theme breakpoints, and resizeResource to build responsive media components
Serving one image file to every device wastes bandwidth on phones and looks soft on large retina displays. Elements gives you three separately-documented APIs that, combined, solve this properly: rw.responsiveProps tells you what size the user wants at each breakpoint, rw.getBreakpoints() tells you where those breakpoints sit in pixels, and rw.resizeResource() manufactures an image file at any width you ask for. This guide shows how real components weave the three together — into <picture> elements, breakpoint-keyed class strings, and responsive background video.
This guide dissects the Image (com.realmacsoftware.image), Text Wrap (com.elementsplatform.shapes), and Container (com.realmacsoftware.container) components from the open-source Core Pack. Open their source alongside this page.
The Three APIs
Each API answers one question, and none of them is useful for responsive media on its own:
The user's per-breakpoint values for any responsive property control
{ base: 400, md: 800, lg: 1200 } — only explicitly set breakpoints appear
The active theme's breakpoint names and pixel widths
names: ["sm", "md", "lg", "xl", "2xl"], screens: { sm: 640, md: 768, … }
A resized variant of a project image resource
Returns a path string to the resized file
The mental model: rw.responsiveProps holds intent ("400px wide on phones, 1200px on desktops"), rw.theme.breakpoints holds geography (where "phones" ends and "desktops" begins for this theme), and rw.resizeResource does the manufacturing. Your hook crosses intent with geography to decide which variants to manufacture, and your template emits them as <source> elements or class strings.
Two supporting facts shape everything below. First, every property control is responsive by default — for class-valued controls Elements prefixes Tailwind modifiers automatically, and you never touch these APIs (see Responsive). You only reach for rw.responsiveProps when the raw per-breakpoint values need processing in hooks — a pixel width to resize by, a boolean to branch on. Second, rw.resizeResource takes a resource object (from a resource control), not a URL — the full object structure is covered in Working with Resources and Image Resources.
Worked Example: The Core Image Component
The Image component's "Sizing" controls offer an Original/Custom choice (imageSizingType), and when the user picks Custom, a responsive File Size number control (imageFileSize, default 400) appears. Because that control stays responsive, the user can dial in a different pixel width at every breakpoint — and the hook turns each one into a real resized file.
The hook starts by gathering all three ingredients:
// hooks.source.js (com.realmacsoftware.image) — excerpt
const {
image,
imageSizingType,
// …
} = rw.props;
const {
imageFileSize,
// …
} = rw.responsiveProps;
const { breakpoints } = rw.theme;
const { names, screens } = breakpoints;
const wantsCustomSizing = imageSizingType == "custom";Then it crosses imageFileSize with the theme's breakpoints to build one <source> descriptor per breakpoint the user actually configured:
Four details here are worth stealing:
Filter before you manufacture.
.filter((name) => imageFileSize[name])skips breakpoints the user never set —rw.responsivePropsonly contains explicit values, so an untouched control produces just{ base: 400 }and exactly one variant.Sort largest-first. A browser walks
<source>elements top to bottom and uses the first match. Withmin-widthmedia queries, the widest query must come first, or a desktop browser would match the mobile source and stop. Every sources pipeline in this guide carries the same.sort((a, b) => screens[b] - screens[a]).Double for retina.
imageFileSize[name] * 2requests a file at twice the display width, so the image stays sharp on HiDPI screens.Report honest dimensions.
Math.min(imageFileSize[name], resource?.width || Infinity)clamps the declared width to the resource's intrinsic width, and the height is derived from the real aspect ratio — so thewidth/heightattributes reserve the correct box and prevent layout shift.
A companion helper, generateDefaultSrc, does the same for the fallback <img>, resizing to imageFileSize.base * 2 and clamping the declared dimensions the same way. The template then emits the whole structure as a <picture> element:
(The shipping template carries additional dark-mode sources, lightbox wiring, and image-protection attributes between these lines — trimmed here.) The division of labour is total: the template contains zero sizing logic. It loops whatever sources array the hook produced and falls back to a plain <img> for browsers — and breakpoints — that no <source> matched.
Breakpoint-Keyed CSS and Class Generation
Automatic responsive prefixing works when a control's value is a Tailwind class. The core Text Wrap component (com.elementsplatform.shapes) — which floats media inside flowing text and wraps the text around the image's actual silhouette — hits the two cases where it can't work, and shows the manual escape hatch for each.
Case one: the value can never be a class. shape-outside takes a url(…) pointing at the image (often a data: URI holding encoded SVG). That string is unsafe as a Tailwind class name and would never exist in the compiled stylesheet anyway, so the hook builds an inline style instead:
The template drops {{mediaStyle}} into a style attribute. Rule of thumb: dynamic values that Tailwind's compiler can't see at build time (URLs, computed numbers, user-entered strings) become inline CSS; everything else stays a class.
Case two: the class depends on another value at the same breakpoint. Which margin side gets a class depends on which way the media floats — and both float side and margin are responsive. The hook resolves each per breakpoint, using a helper that reimplements the mobile-first cascade over the sparse rw.responsiveProps object:
Because rw.responsiveProps only contains explicitly set breakpoints, resolveResponsiveValue walks downward from the requested breakpoint until it finds a value — exactly how min-width media queries inherit in CSS. With those two helpers, the main routine iterates ["base", ...names], resolves float and margin at each stop, and emits prefixed classes only where something changed:
Note the prevFloat tracking: when the float side flips from right to left at some breakpoint, the hook must both add the new mr-* margin and zero out the old one with ml-0 — the kind of pairwise dependency automatic prefixing can never express. The result is an ordinary class string (mr-4 mb-4 md:mr-0 md:ml-6 …) merged into the media element's class list.
Scaling It Up: A Responsive Card Image
The same crossing generalises to any component that renders images at a user-controlled size. Here is a compact com.example.cardImage that exposes one responsive Thumbnail Width control and builds a full breakpoint-to-variant map from it. The control is a plain number — responsive by default, so no extra configuration is needed:
If the user leaves the control alone, thumbWidth is { base: 400 }, the sources array is empty, and the component ships a single 800px file. The moment they set a width at md and lg, two <source> variants appear — no template changes, no new controls. To scale this across a whole gallery, run the same sources-building function once per resource inside a map over the collection (see Galleries & Resource Collections) — but read the variant-cost pitfall below first.
Responsive Background Video
Video can't be resized the way images can, so the core Container component makes its background video responsive with a different toolkit: fill-the-box CSS plus per-format template selection. The hook reduces the video resource's format field to one boolean per embed mechanism — precomputed because @if and @includeIf take a single condition:
The template then picks exactly one partial per format with @includeIf:
Each partial in templates/include/ renders its own embed markup. The MP4 one is the simplest:
The responsiveness lives entirely in w-full h-full object-cover: the video fills whatever box the container occupies at any viewport and crops overflow, so no per-breakpoint variants are needed. Note the video file arrives via bgVideo.video.path — the path field, not image, is how non-image resources expose their file (see Image Resources) — and the autoplay attributes (muted, playsinline) are exactly the set mobile browsers require before they'll autoplay anything.
Pitfalls
rw.resizeResource works on project resources, not URLs. Its first parameter is an image resource object from your properties or collections — a pasted remote URL or a CMS field token has no local file to resize. The core Image component respects this split: only imageType == "resource" images flow through generateResourceSources; custom-URL and CMS images pass their per-breakpoint URL strings straight through to <source> elements unresized.
Get the <source> order and media queries right. The browser commits to the first matching <source>, so min-width queries must be sorted widest-first, and the query should describe the layout breakpoint, not the image. If your card sits in a three-column grid above lg, the image displayed there is roughly a third of the viewport — size the lg variant for the rendered slot, not the full screen width. Declared width/height attributes should describe display size, clamped to the resource's intrinsic width, or you'll reserve the wrong box and cause layout shift.
Every variant is a manufactured file. Each distinct rw.resizeResource(resource, width) call produces an image at export. One hero image across five breakpoints is six files; a 60-image gallery crossed with five breakpoints is hundreds. Follow the core Image's lead: filter to breakpoints the user explicitly set, and for galleries consider one shared variant width per breakpoint rather than per-image sizing.
Edit mode changes what you have to work with. CMS field tokens can't resolve inside the editor, so both Image and Text Wrap swap in a shared placeholder (${sharedAssetPath}/images/image-square.png) when rw.project.mode is "edit", and Image disables its lightbox there entirely. Check the mode before wiring behaviour that only makes sense on a published page — Designing the Edit-Mode Experience covers the full pattern.
Related Documentation
Last updated
Was this helpful?

