Munchable is a barcode scanner for people with digestive conditions. You point a phone at a pack and it answers for your own set of conditions. The whole product is one look, which the team notes call "Cookie and Cream": a cream base, cocoa ink, cookie gold and caramel for the playful parts, one pink pop for sprinkles.
You can see it on the landing page, in a condition guide like the IBS and low FODMAP one, and on a recipe page such as gentle rice congee. Keep one of those open, because the point of this post is that a repository can hold several descriptions of its own colours, and only the live pages tell you which description won.
Three surfaces, one look
Three things have to look like Munchable: the Expo app, the marketing site, and the Remotion compositions that render a phone running Munchable for social video.
The third one is why a token package exists at all. The reels used to carry a hand-copied palette under a comment promising it mirrored the app:
/**
* The reels' look is the app's look.
*
* The palette, the scales and the verdict vocabulary are IMPORTED from
* @munchable/design-tokens ... They used to be hand-copied here under a
* comment promising they mirrored the app, which is exactly the arrangement
* that drifts: changing the caution colour or the "Can't assess" label in the
* app left the reels rendering the old one with no type error.
*/
That is the failure worth designing against. Nothing breaks, nothing fails to compile, and the marketing video shows last month's product.
So packages/design-tokens holds the platform-free half: the palette, spacing, radii, the type scale as sizes only, and the verdict vocabulary. apps/mobile/src/theme/tokens.ts re-exports all of it and adds the parts that need React Native, which are the motion curve, the shadow, and the font-family names Expo loads. The reels import the same module. App code still has one import path, and a phone in a video is painted from the real values.
Two decisions in that file are worth pulling out.
The contrast ratio of every pairing is written next to the value, so a colour cannot be nudged without reading what it costs:
text: '#3B2314', // cocoa, cream 13.9:1
textMuted: '#7F5F47', // warm brown, cream 5.5:1
link: '#9A5F1C', // deep caramel, cream 4.9:1
accent: '#FF8FAB', // decoration only: it fails contrast as text
And there is exactly one palette:
/** The one and only palette. Munchable is light mode only. */
export const palette: Palette = light;
No dark object, no theme switch, no useColorScheme. A light-only product is a real decision with real costs, and writing it as one exported constant means nobody has to wonder whether the dark half is missing or unfinished.
The website does not import any of it
The site is plain CSS. Its colours live in 28 custom properties at the top of a 3,144-line stylesheet:
:root {
--primary: #5c3417;
--ink: #3b2314;
--bg: #fff8ec;
--bg-warm: #f5ead8;
--good: #15803d;
--caution: #b45309;
--avoid: #dc2626;
--r-sm: 12px;
--r: 18px;
--r-lg: 28px;
}
That is defensible. Custom properties are the web's own token mechanism, cascade into every rule for free, and pulling a TypeScript module into a stylesheet means adding a codegen step and a generated file to review. The cost is that agreement between the site and the app is now a convention rather than a type.
So I audited it while writing this, and the result is split cleanly in two.
Every colour agrees, exactly. --bg is the package's bg, --ink is text, --primary is primary, and the three verdict colours match to the digit.
The radii do not agree at all. The package says 8, 12, 16, 22. The stylesheet says 12, 18, 28. Those are not roundings of each other, and no comment in either file claims the site is deliberately rounder than the app. The site's cards are physically bigger, so a bigger radius may well be the right call, but right now the only evidence that it was a call is that somebody typed it.
There is also a colour the site does not have. The package defines four verdict colours, and the fourth one carries a rule:
/** Solid verdict colour. `unknown` is deliberately grey, never green. */
The stylesheet has three. The marketing pages never render the fourth state, so nothing was missing, but it does mean the one verdict colour with a written-down rule attached is the one living in a single file.
The third copy, which is not a copy of anything
While looking, I found a docs/design-tokens.json. It opens like this:
"description": "Poppy, fresh-produce-energy design system for the Munchable
gut-health app. Machine-readable source of truth; codegen'd into the RN
theme."
And docs/DESIGN-SYSTEM.md beside it says, in bold, that when the two disagree the JSON wins and the prose is stale.
Its palette is green. bg is #FBFDF6, text is #152318, primary is #0A8049, and the avoid colour is described in the prose as a friendly watermelon coral. It is a complete, carefully reasoned design system for a product that now looks nothing like it, from before the cookie mark existed.
My favourite part is that the shipped palette has a comment forbidding precisely what the stale file prescribes:
// The brand primary is deliberately not green so a button never reads as "good".
Nothing generates anything. grep -rl design-tokens.json over the repository returns exactly one file: the Markdown document that points at it. There is no codegen step, there never was one in this repository, and the React Native theme it claims to produce is a hand-written file importing a TypeScript package.
What I take from it
A token package is worth it for the surface it can actually reach. Moving one palette out of the app and into a package deleted a whole class of silent drift between the app and the video renderer, and it did it with types rather than vigilance.
It cannot reach a stylesheet, and the honest version of that is to say so, which is why the radius mismatch is in this post rather than quietly fixed in the same commit. The fix I want is a test that reads both files and asserts the shared values, not a migration of the site onto a JavaScript theme.
The JSON is worse than the mismatch. A document claiming to be generated reads like a build step, so the next person to touch the design either edits the wrong file or stops trusting documents. Two sources of truth that disagree is a bug. A third that lies about being generated is a trap, and deleting it is one line in a commit nobody will miss.
Open munchable.app and you can settle which palette shipped in about three seconds. That is the part a repository cannot argue with.
Top comments (0)