DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Our landing page has four phone screenshots and zero images

Open munchable.app and scroll past the hero. There are four phones down the page, each showing a screen of our app: the scan result with its verdict and reasons, the "check your photos" step of label capture, the hub in menu mode, and the recipe library.

Now run this:

curl -s https://munchable.app/ | grep -c "<img"
# 0
Enter fullscreen mode Exit fullscreen mode

Zero. The whole site ships exactly one raster file, app/opengraph-image.png, and that one is for social cards rather than for any page. The phones are markup.

Why a screenshot was the wrong primitive for us

A screenshot of your own product is a fact with an expiry date. The app ships, the screen changes, the PNG does not, and nothing in your build says a word about it. There is no type error for a stale image. We had already been bitten by exactly that class of bug one layer over: our marketing videos used to carry a hand-copied mirror of the app's theme tokens, and changing a colour in the app left the videos rendering the old one, silently, forever. That is the whole reason the tokens are their own package now.

Then there is the responsive matrix. A phone mock that has to look sharp means two or three pixel densities, and because the mock contains text, it also means the text inside your page does not scale with the reader's font size. A reader who browses at 200% gets your copy at 200% and your screenshot's captions at 100%, blurred.

And a screenshot is opaque to the page it sits in. It cannot inherit the palette, cannot respond to a media query, cannot be selected, cannot be searched, cannot be diffed in a pull request in any way a human can read. "Changed one pixel of a 400 KB binary" is not a reviewable change.

What they actually are

Each phone is a component rendering the same markup grammar the real screen uses, styled by the .pm-* rules in our one stylesheet. Those rules deliberately mirror the hero's .sd-* rules at a slightly smaller size: 86 .pm- rules and 84 .sd- rules out of 3,123 lines of CSS, which is the kind of near-duplication that is cheaper to keep than to abstract.

Inside the result phone, the verdict row is a row, the confidence badge is a badge, and the reason is a sentence:

<div className="pm-crows">
  <div className="pm-crow">
    <i className="pm-dot avoid" />
    ...
  </div>
  <p className="pm-reason">High fat per serving is a common reflux trigger.</p>
Enter fullscreen mode Exit fullscreen mode

That sentence is in the HTML you just curled. Select it with your mouse on the live page. You can highlight the text inside the phone, which you cannot do with a screenshot, and it is a surprisingly effective way to prove to a visitor that they are looking at the thing rather than at a photo of the thing.

The part that gets argued about: every phone is aria-hidden

<div className="pm" aria-hidden="true">
Enter fullscreen mode Exit fullscreen mode

All four of them. A screen reader user is told nothing about the contents of these phones.

That looks like the wrong call for about five seconds. It is the right one, because pseudo-UI built out of real DOM nodes is a trap. Rendered honestly to assistive tech, a fake result screen announces a product name that does not exist, a verdict nobody asked for, a button that does nothing when activated, and a confidence badge about data that was never fetched. The reader is handed a tour of a mock.

So the mock is hidden and the sentence beside it does the work. Every row has prose next to the phone that says what the phone shows, in full, as a claim rather than as a caption:

Pick the conditions you live with and Munchable weighs every ingredient against all of them at once, then reconciles the result into one verdict with the reasons under it.

The test we hold ourselves to is simple: turn off CSS, or read the page with images and decoration gone, and the page still says everything it was trying to say. The phones are evidence, not content.

Stills, not animations

The hero already animates a scan. Four more looping phones underneath it turns a landing page into a wall of screensavers, and every one of them is work the device has to do while someone is trying to read a paragraph. So the feature rows are frozen frames, which also means there is no scroll-driven animation to get wrong for a reader who asked for less motion. We wrote about where that preference leaks in the blanket reduced-motion rule has a hole in it.

What this does not fix

A hand-built screen can drift from the app exactly like a PNG can. Rebuilding the screen in markup buys reviewability, weight and accessibility control; it does not buy truth. Two rules do that work:

  1. Anything with a colour, a radius or a verdict word in it comes from the shared token package, so the app and the page and the videos cannot disagree about what "caution" looks like or what it is called.
  2. No mock may show an answer the product would not actually give. Our videos follow the same rule and go further, because they compute their verdicts rather than quoting them.

The second rule is a review rule, not a test, and it is the honest weak point of the whole approach.

Where we do use a screenshot

Social cards. A crawler fetching our Open Graph image cannot execute our CSS, so that one has to be a real raster file, and ours is produced by driving a headless browser over a route we built for the purpose: our OG card is a Playwright screenshot. The rule is not "never rasterise". It is "rasterise at the boundary where nobody can run your code, and nowhere earlier".

Try it

  • munchable.app/#how is the four rows. Select the text inside a phone.
  • Zoom the page to 400% and watch the phone screens stay sharp while reflowing.
  • curl -s https://munchable.app/ | grep -c "<img" is the claim, and it is checkable in a second. The same page has 55 inline SVGs and weighs under 100 KB of HTML.

Top comments (0)