When Screenshots Lie: Rebuilding Product Imagery as Code
Shopify’s marketing team faced a familiar problem during a 2019 site overhaul: visitors wanted real product visuals, but the screenshots on shopify.com were outdated. One example showed the old Shopify POS software, misrepresenting a product that had since been updated and rebranded.
An SVG-based approach was tried first as a stopgap. It worked, but only for a handful of "high-value" pages, and it introduced a host of new problems. The team recreated the visuals in HTML and JavaScript instead, developing a utility called ScaleContentAsImage that resizes an element's contents proportionally to its container — something CSS alone cannot do since font sizes only scale with the viewport, not a parent element.
The Localization Problem
The visuals needed to be translated across more than 35 localized domains, each with different currencies, features, and languages. Re-capturing screenshots per domain to stay in sync with product changes was impractical.
Designers settled on simplified UI illustrations to focus attention on specific product features. These solved the fidelity problem but introduced a maintenance one: each translation and product version required separate image assets. Shopify had automated translation updates in code but not in the design editor.
Why SVG Didn't Scale
Exporting these visuals as inline SVGs and swapping in translated text worked in principle but came with significant drawbacks:
- Gaussian blur effects caused performance issues in Firefox.
- SVG text doesn't wrap at a max-width like HTML, so longer languages like German overflowed their containers with broken-looking results.
- Export settings had to be consistent across every developer or entire SVG structures would shift with each new export.
- Every export meant manually replacing text with translation hooks again.
A month in, the process demanded heavy documentation just to keep it consistent, and new Sketch settings kept surfacing as issues. The team was replacing one arduous process with another.
The HTML/JavaScript Replacement
ScaleContentAsImage determines the available space for a visual and resizes its contents to fit. The implementation requires a few distinct steps:
- Store the element's computed width. The class constructor accepts the DOM element and saves its fixed pixel width, which matches the visual's width in the design file.
- Set the element to
100%width. The class overrides the stored width so the element fills available space. - Create an unscaled wrapper. A new element gets the original fixed width as a CSS transform target; it isn't yet added to the DOM.
- Move content into the wrapper. All children transfer into the new element via methods that preserve existing event bindings, such as lazy-load listeners.
- Apply the scale transform. If the visual was designed at 200px but renders in a 100px container, the wrapper gets
transform: scale(0.5).
CSS transforms don't affect document flow, so the scaled element still reserves its original fixed width in the layout. To fix this, the implementation adds an empty spacer element that preserves the correct vertical space using an aspect-ratio technique, and absolutely positions the scaled element so it no longer interferes with layout calculations.
Resize handling updates these calculations when the window changes. Unless the visual contains lazy-loaded images or animates vertically, this only needs to run once.
In a React project, the same behavior is simpler since React manages DOM creation and updates. The only remaining logic is calculating available container space, and CSS-in-JS passes the fixed width directly into the element.
Localization Has a Currency Problem
Displaying fictional prices in local currencies surfaced an unexpected issue. A visual showed two chairs at ~$200 each. Swapping the currency symbol alone broke realism: 200 Japanese Yen is under $1.89 USD. The team created a one-time table of conversion rates to keep the fictional prices plausible in any localized market.
These rates aren't updated regularly. Rough accuracy is acceptable for invented products whose purpose is demonstration, not commerce. The team notes this approach doesn't apply to real products or prices.
What the HTML/JS Approach Won
Comparing HTML/JS against SVG across several dimensions showed clear advantages:
- Text: HTML supports word wrapping that SVG lacks, preventing the overflow seen with German translations.
- Implementation: Developers code with existing components rather than learning SVG quirks. The code is parseable and testable with existing tooling. SVG typically produced fewer lines due to inline styles and absolute positioning, but that hardly justifies a less maintainable solution.
- Animation: Both support animation, but HTML/JS integrates with existing play/pause controls.
- Accessibility: SVG works without JavaScript but degrades performance with shadow effects and similar properties. HTML/JS doesn't rely on SVG rendering.
- Design: Designers no longer need to follow a specific process or tool chain. No more enforcing consistent Gaussian Blur settings or designing around SVG's text-wrapping limitations.
- Documentation: The team discarded extensive SVG export guidelines, design specs, and quirk notes. The only HTML-specific documentation needed covers applying the resize utility.
The upfront investment in ScaleContentAsImage paid off quickly once the first visuals were built. Other projects began adopting the approach for their own product illustrations, suggesting the strategy extended well beyond the original localization problem.
From a Name to a Container
The initial implementation of the visual system centered on a single component that took a name—like Shopify admin dashboard or POS software—and rendered the corresponding illustration. This central entry point was meant to keep the set of UI Illustrations small, maintainable, and easy to track. Every new visual came with its own tests and documentation.
That approach worked, but it created an awkward maintenance burden. Each illustration is just HTML markup used in one place, yet a developer had to maintain structural code, documentation, and tests for basic markup. The effort spent educating contributors on the structure was disproportionate to the value of the visual itself.
The solution was to generalize. Instead of tying ScaleContentLikeImage to a single monolithic component, Shopify provided a generic container that can wrap any block of HTML for initialization. This container also delivers a consistent implementation of descriptive text for screen readers, removing the need to re-implement accessibility behavior for each visual.
What Still Needs Work
The current HTML and JavaScript approach serves the team well, but there are known gaps. Recalculating size for lazy loaded images still carries performance costs, and the team is still figuring out how to document existing visuals so they can be easily reused across pages.
A pure CSS solution would be cleaner, but none exists today that can handle this use case. One promising direction is the CSS Houdini Layout API, which is not yet supported in any major browser. Based on demos from the community, such as Anton Dosov’s video gallery, the idea would be to write a custom layout renderer and apply the scaling logic with just a few lines of CSS.
Until that becomes viable, the HTML and JavaScript combination remains the most pragmatic choice, and the system will keep evolving as the team refines its implementation and documentation practices.



