CogniPrep is on Tailwind v4, which means there is no
tailwind.config.ts in the repository at all. The theme is CSS, in
app/globals.css, 719 lines of it, and it starts like this:
@import 'tailwindcss';
@custom-variant dark (&:is(.dark *));
Then 24 colour custom properties in :root, 24 again in .dark, and a second
@theme block that hands them to Tailwind so they become utilities:
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
/* ... */
--color-ring: var(--ring);
}
There are 19 lines in that block and 24 tokens. The five that did not make it
are --chart-1 through --chart-5, and this post is mostly about what happens
to a token that exists everywhere except in @theme.
A custom property is not a colour
The five chart tokens are declared in both themes, so they resolve perfectly
well at runtime. On any page of the live site:
getComputedStyle(document.documentElement).getPropertyValue('--chart-2')
// "#2eb88a"
What they are not is part of the --color-* namespace, and that namespace is
what makes bg-*, text-*, from-* and via-* exist. So this class, which
appears at eight call sites in our dashboard and landing components, is not a
class:
<div className="bg-chart-2/10 absolute bottom-0 left-0 h-32 w-32 rounded-full blur-3xl" />
<Card className="border-primary/20 from-primary/5 via-chart-2/5 to-primary/5 bg-gradient-to-br" />
The whole site is served by one stylesheet, the same two CSS chunks on the
landing page and on /login, so you can check this from outside with no
account. The token is in there. The utility is not:
curl -s https://cogniprep.app/ \
| grep -o '/_next/static/chunks/[a-z0-9-]*\.css' | sort -u \
| while read c; do curl -s "https://cogniprep.app$c"; done > /tmp/cp.css
grep -o -- '--chart-2:[^;]*' /tmp/cp.css | head -1
# --chart-2:#2a9d90 (the :root value; the .dark block further down says #2eb88a)
grep -c 'bg-chart' /tmp/cp.css
# 0
Zero rules. Tailwind v4 generates utilities from what it finds in your source,
and an unregistered namespace is not an error, it is just nothing: no warning
at build time, no missing-class failure, no entry in the output. The class
attribute keeps the string, the element keeps its blur-3xl and its
rounded-full, and the colour it was supposed to wash the card with is
absent.
Which is the second half of why this survived: every one of the eight is
decorative. A 10 percent tint behind a card, a blurred circle in a corner, two
gradient stops out of three. Nothing looks broken, it just looks slightly
flatter than the person who wrote it intended. The fix is one line in the
@theme block, and the lesson is that in v4 the thing to grep for is not the
class name, it is whether the token is in the colour namespace.
24 tokens, 15 colours
While reading the compiled stylesheet, the other thing it tells you plainly is
how many of the 24 tokens are distinct. Light theme, lifted straight out of the
live CSS:
foreground, card-foreground, popover-foreground #020817
card, popover #fff
primary, ring #2463eb
primary-foreground, destructive-foreground #f8fafc
secondary, muted, accent #f1f5f9
secondary-foreground, accent-foreground #0f172a
border, input #e2e8f0
15 distinct values across 24 tokens. The dark theme is tighter still at 14, and
the collapse there is dramatic:
foreground, card-foreground, popover-foreground, primary-foreground,
secondary-foreground, accent-foreground, destructive-foreground #fafafa
secondary, accent, input #2e2e2e
card, popover #1f1f1f
primary, ring #3c83f6
Seven of the dark tokens are the same white. That is inherited from the shadcn
default set and it is not wrong, but it does mean the token layer promises more
semantic separation than the palette behind it delivers: bg-secondary,
bg-muted and bg-accent are three names for one colour in both themes.
The usage counts show which name won. Across app/, components/ and lib/:
| class | call sites |
|---|---|
text-muted-foreground |
3,428 |
text-foreground |
1,846 |
bg-muted |
1,158 |
border-border |
731 |
bg-card |
645 |
text-primary |
510 |
bg-primary |
295 |
bg-accent |
23 |
bg-secondary |
4 |
bg-muted and bg-secondary paint the identical pixel, and one of them is
used 290 times as often as the other. The redundancy is harmless precisely
because nobody reaches for the redundant half, which is also why nobody noticed
that five other tokens do not work at all.
794 dark: variants on a layer built to flip
The token design means a component should not need a dark: override: swap the
class on <html> and every semantic colour changes underneath it. There are
794 dark: utilities in the TSX anyway, 773 of them in components/ and 21 in
app/, and the compiled stylesheet carries 160 distinct ones.
They are not fighting the tokens. Almost all of them are raw palette accents
that have no semantic token at all, amber for a warning strip, blue for an
informational panel, and those genuinely do need a hand-written dark value.
What is interesting is where they are: not here. The public pages run dark
unconditionally, from a path list the root layout consults, so on the landing
page, /pricing and /blogs:
document.documentElement.classList.contains('dark') // true
document.querySelectorAll('[class*="dark:"]').length // 0
document.querySelectorAll('[class*="chart-2"]').length // 0
A reader of the public site sees the token layer doing exactly what it was
designed for, and none of the 794 overrides. Both the overrides and the five
broken tokens live behind the login, which is a decent description of where a
design system quietly drifts: on the pages you do not open first thing in the
morning.
See it
The two curl commands above work on any page of
cogniprep.app, because the stylesheet is global. The
three browser lines work in the console on
cogniprep.app/pricing or
cogniprep.app/blogs. If you want the token
values rather than the utilities, read them off the root element:
['background','primary','muted','secondary','accent','chart-2']
.map(t => [t, getComputedStyle(document.documentElement).getPropertyValue('--' + t).trim()]);
--muted, --secondary and --accent will come back identical, and
--chart-2 will come back with a colour that no class in the stylesheet can
apply.
Top comments (0)