Notifio is two codebases that have to look like one product: a Next.js marketing site, and an Electron desktop app whose UI is React. Both are dark, both use Tailwind, and the app is supposed to feel like the thing the website is showing you.
Neither of them defines a single colour token. I went and counted what they define instead.
grep -rhoE '#[0-9a-fA-F]{3,8}\b' server/src app/src | wc -l 1,216 hex literals. 62 distinct values. 887 on the website, 329 in the desktop renderer, and 92 of the website's inside seven HTML email templates.
Both projects use Tailwind v4. The website's @theme block declares three fonts and no colours, and the desktop app's stylesheet has no @theme block at all:
@import "tailwindcss"; @theme { --font-sans: var(--font-dm-sans), ui-sans-serif, system-ui, sans-serif; --font-mono: var(--font-geist-mono), ui-monospace, monospace; --font-display: var(--font-space-grotesk), ui-sans-serif, system-ui, sans-serif; } So every colour in the product is an arbitrary value: text-[#a3a3a3], border-[#262626], bg-[#0a0a0a], 1,216 times.
70 per cent of them are one ramp
The 62 values are not 62 decisions. Sorted by use, the top of the list is extremely regular:
#fafafa 240 #525252 165 #a3a3a3 138 #404040 76 #0a0a0a 74 #262626 61 #737373 41 #e5e5e5 41 #171717 16 #d4d4d4 5 That is Tailwind's neutral ramp, in order: 50, 600, 400, 700, 950, 800, 500, 200, 900, 300. Ten rungs, 857 of the 1,216 literals, 70 per cent of every colour in the product, and the share is almost identical in each codebase: 70 per cent on the website, 72 per cent in the desktop app.
Which is a good result, in a way. Nobody wrote a design system, and a ramp emerged anyway, because the person typing #525252 into the next component was copying it from the last component. The palette is consistent by imitation.
The other 30 per cent is where it gets interesting.
- 243 literals are greys that are not on the ramp. Twenty distinct values, mostly sitting between its rungs:
#1a1a1a54 times,#1e1e1e50,#11126,#3a3a3a24,#0f0f0f21,#14141416,#2a2a2a11, then a tail of#0d0d0d,#1c1c1c,#252525,#2e2e2e,#161616,#222,#383838. The ramp jumps from#171717to#262626, which is a big step at the dark end, and a dark UI needs about four surfaces in that gap. So they got mixed by hand, one at a time, by eye. - 116 literals are accents, 32 distinct, and these are almost all in the desktop app rather than the website. The website shows a product; the app shows state, and state needs green for running, amber for paused, red for a failure, plus a dark tinted background and a mid-tone border for each. That is three colours per status before you have styled anything, and it is why the app knows 24 colours the website has never heard of.
Meanwhile the seven email templates use exactly 8 values, all of them on the ramp, and that is the one place the discipline is enforced by the medium: inline styles in a table layout, no cascade, no variables, so every colour is typed at every use and you quickly stop inventing them.
The rung that moved
Here is the part I did not expect. I converted every colour the installed tailwindcss@4.3.3 ships, out of its theme.css, from oklch to sRGB, and compared.
Nine of our 62 values are still exact Tailwind colours. The tenth rung of our ramp is not:
ours tailwind 4.3.3 #fafafa neutral-50 #fafafa ✓ #e5e5e5 neutral-200 #e5e5e5 ✓ #d4d4d4 neutral-300 #d4d4d4 ✓ #a3a3a3 neutral-400 #a1a1a1 ✗ #737373 neutral-500 #737373 ✓ #525252 neutral-600 #525252 ✓ #404040 neutral-700 #404040 ✓ #262626 neutral-800 #262626 ✓ #171717 neutral-900 #171717 ✓ #0a0a0a neutral-950 #0a0a0a ✓ #a3a3a3 was neutral-400 in Tailwind v3. In v4 the ramps are defined in oklch, neutral-400 is oklch(70.8% 0 none), and that resolves to #a1a1a1. Two units of grey, on the one value we use 138 times, which makes it the third most common colour in the entire product and the colour of most of the body text on the site.
You can check the claim in one line:
grep -o -- '--color-neutral-400: [^;]*' server/node_modules/tailwindcss/theme.css # --color-neutral-400: oklch(70.8% 0 none) What that costs is not a visual bug. Nobody will ever see two units of grey. What it costs is the refactor. The obvious cleanup here is to delete the arbitrary values and use the utilities, and for nine of the ten rungs that is a pure sed:
text-[#525252] -> text-neutral-600 border-[#262626] -> border-neutral-800 bg-[#0a0a0a] -> bg-neutral-950 For the tenth it is a colour change, applied to 138 call sites, inside the same commit as 719 mechanical substitutions. That is precisely the commit shape you cannot review: a diff where every line looks identical in kind, one group of which quietly changes pixels. And #a3a3a3 is the single value you least want to change blind, because it is the text colour of the standfirst and the body paragraphs in the shared article layout, which is to say most of the prose under notifio.app/guides.
What I would actually do
The ramp already exists. It just lives in people's fingers instead of in a file. Writing it down costs about a dozen lines:
@theme { --color-surface: #0a0a0a; --color-surface-1: #141414; --color-surface-2: #1a1a1a; --color-border: #262626; --color-text: #fafafa; --color-text-muted: #a3a3a3; --color-text-dim: #525252; } Note what that does with the awkward parts. It keeps #a3a3a3 as a literal, under a name, rather than aliasing it to neutral-400 and taking the shift. And it gives the hand-mixed in-between greys somewhere to live, which is the actual reason there are twenty of them: #1a1a1a and #1e1e1e and #1c1c1c are three people independently answering the question "what is the surface one step above the page background", because that step had no name.
The second thing I would do is give up on sharing it. The website and the desktop app agree on 17 of their 62 values, and the overlap is the ramp, almost exactly. Everything the app has in addition is status colour, which the website structurally cannot use, and the shared bit is a ten-entry list that is stable enough to copy. A package that two apps import to keep ten hex values in step is more machinery than the problem has.
The place this discipline actually pays is the one I did not plan: those 8 colours in the email templates. An email is the only surface in this product where a colour cannot be a variable, cannot be a utility class, and cannot be changed after it is sent. Seeing that the emails use 8 values and the UI uses 62 is the clearest possible statement of which parts of a product are allowed to drift.
If you want to see the ramp in use rather than in a table, it is every surface on notifio.app, and the #a3a3a3 in question is the standfirst and every body paragraph on notifio.app/guides/how-fast-do-rental-listings-go.
