DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Zero img tags on our landing page, and four of the phones on it are app screens

Munchable is a gut health scanner: you scan a barcode, it weighs the ingredients against the conditions you manage and gives you one verdict with the reasons under it. The landing page has to show that, because "it gives you a verdict" is a sentence anybody could type and nobody believes.

The obvious way to show it is screenshots. We have none. Fetch munchable.app and count the image tags:

$ curl -s https://munchable.app | grep -o "<img" | wc -l
0
$ curl -s https://munchable.app | grep -o "<svg" | wc -l
55
Enter fullscreen mode Exit fullscreen mode

Every phone on that page is markup, and the "photo of an ingredients panel" inside one of them is an inline SVG. This post is about why, and about the three decisions that made it work rather than look like a placeholder.

Why not screenshots

A screenshot is a raster image of a moving target. Ours would have been wrong within a fortnight, because the app is being changed daily, and a stale screenshot is worse than a diagram: it is a specific promise about a screen that no longer exists.

Then there is the frame problem. A bare screenshot looks like a slide, so you put it in a phone bezel, and now the bezel is an asset too, at three densities, in a page whose whole visual identity is warm cream paper. Ours came out as a flat white card with a wordmark in the corner, which is what the code comment in FeatureDemos.tsx still complains about:

They used to be flat white "verdict cards" with a Munchable wordmark in the corner, and all three were the same card with different words in it. Nothing on them looked like the app, so a reader who had just watched the hero's phone scan a soup carton got a change of product halfway down the page.

That last clause is the real cost. The hero already animates a phone, drawn in CSS, and it sets the reader's expectation for what the product looks like. Anything further down the page that does not match that phone reads as a different product.

One phone primitive, two sizes

The hero's phone is a set of .sd-* rules. The feature rows are .pm-* rules that mirror them at a slightly smaller size, so the two cannot drift in shape:

.sd-phone {                 /* hero */          .pm-phone {              /* rows */
  width: 336px;                                   width: 300px;
  padding: 10px;                                  padding: 9px;
  border-radius: 46px;                            border-radius: 42px;
  background: #2a180d;                            background: #2a180d;
  box-shadow: var(--shadow-lg),                   box-shadow: var(--shadow-lg),
    inset 0 0 0 1.5px rgba(255,255,255,.07);        inset 0 0 0 1.5px rgba(255,255,255,.07);
}                                               }
Enter fullscreen mode Exit fullscreen mode

The screen inside takes background: var(--bg), the site's own cream, so the mocks are not painted with hardcoded colours that a theme change would strand. The app's real cream lives in our design tokens package and the marketing site deliberately does not import it, for reasons I wrote up separately in Our design tokens are a package, and the marketing site is deliberately not a consumer. The upshot for these mocks is that there is exactly one variable to change per surface, not 38 hex codes scattered through four phones.

Each demo is then a small component inside a shared Phone wrapper:

function Phone({ children }: { children: ReactNode }) {
  return (
    <div className="pm" aria-hidden="true">
      <span className="pm-blob pink" />
      <div className="pm-phone">
        <div className="pm-screen">
          <span className="pm-island" />
          {children}
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Four of those exist: the result sheet, the "check your photos" step of label capture, the hub in menu mode, and the recipe library. The one asset-shaped thing in the set is the label photo, and it is an SVG that draws a cream label rotated 1.6 degrees on a darker pack, with the ingredient lines as real <text> nodes. It reads as a photographed panel and stays sharp on any display, which no 2x PNG of ours managed.

Still, and hidden

Two decisions that took longer to agree than the CSS did.

The four phones do not animate. The hero moves; that is the page's one moving thing. Four more looping phones turn a landing page into a wall of screensavers, and the reader stops reading to watch them. Motion is a budget, and the hero spends all of it.

The whole thing is aria-hidden="true". A screen reader working through "Caution, Reflux, Avoid, chevron, Bile acid malabsorption, Caution, chevron" is being handed a transcript of a picture. The paragraph beside each phone is the content, and it is written to stand on its own:

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.

If that sentence needs the phone next to it to make sense, the sentence is wrong. Treating the mock as decoration forces the copy to carry the meaning, which is the same discipline that makes the page work with images off, on a slow connection, and in a search result snippet.

The honest cost

A hand-laid mock can lie. It can show a verdict the engine would never produce, or a layout the app abandoned last week, and nothing in CI will tell you.

We accept that for these four, because their job is to show what a screen looks like, and we keep the claim small: no numbers that imply a calculation, no verdict a reader could check against a specific barcode. Where a verdict does appear in our marketing, in the video ads, it is not typed by hand at all: those compositions import the production rules engine and render whatever it returns, which I wrote about in Our ad creative imports the production rules engine. Mocks for layout, the engine for claims.

Have a look and inspect one: munchable.app, scroll to the feature rows, and open devtools on a phone. There is nothing in there but divs, a few inline SVGs and about 38 class names.

Top comments (0)