D
P
0

CSS & Tailwind

Text With `text-[oklch(0.97_0.01_260/0.80)]` Renders With No Color in Tailwind v4? The Trailing Zero Means the Class Never Compiles

July 4, 2026·4 min read
Text With `text-[oklch(0.97_0.01_260/0.80)]` Renders With No Color in Tailwind v4? The Trailing Zero Means the Class Never Compiles

The symptom read like a layout bug: a section subtitle on a landing page I was working on had shipped to production invisible. Not display: none, not covered by another element. The node was in the DOM, taking up its space, selectable if you knew where to drag. The section behind it was dark, and the subtitle was supposed to get a soft near-white ink from a single Tailwind class:

<h2 class="text-[oklch(0.97_0.01_260/0.80)]">
  A subtitle that shipped invisible
</h2>

What made it stranger: other OKLCH classes on the same page worked fine. The heading right above it used an arbitrary OKLCH value too and rendered perfectly. The build was clean, no errors, no warnings, deploy green. This one class had simply stopped existing.

My first instinct was, as usual, wrong: I assumed a specificity fight, some later rule stomping the color. DevTools said otherwise. Nothing was overriding anything. There was no matching rule at all. The class sat neatly in the class attribute, but the styles panel showed zero rules for it. The computed color was pure inheritance from the parent, the section's dark base color, on top of an equally dark background. Of course it was invisible.

I searched the compiled stylesheet for oklch(0.97. The rule was there. But the selector ended in /0.8), not /0.80):

.text-\[oklch\(0\.97_0\.01_260\/0\.8\)\] {
  color: oklch(0.97 0.01 260 / 0.8);
}

One character. One trailing zero. A grep across the codebase explained the rest:

grep -rn "oklch(0.97_0.01_260" src/

The same color was living under two spellings. In some files the class was hand-written as /0.80; in others it arrived from a pasted snippet as /0.8. Mathematically, identical alpha. As strings, two different classes.

Why this happens

Tailwind v4 does not parse your templates; it scans your source as text, collects candidate classes, and generates CSS for what it finds. Arbitrary values are stringly-typed: text-[oklch(0.97_0.01_260/0.80)] and text-[oklch(0.97_0.01_260/0.8)] describe the exact same color, but to the scanner and to the browser they are two unrelated class names. The engine also normalizes arbitrary values when it writes CSS, so what lands in the stylesheet is a canonical form, and the class string in your HTML has to match that generated selector character for character.

In this codebase, the stylesheet ended up containing a rule for one spelling only. The other spelling, sitting in markup that was very much live, matched no rule at all. And an unmatched utility class is not an error in this model. It is nothing. No failed build, no console warning, no dead-code report. The browser sees a class attribute holding a token that matches no selector and shrugs, exactly as it would at a typo. That is by design: utility scanning has to tolerate unknown strings, so it cannot possibly tell you that one of those strings was meant to be a color.

What makes this bug cruel is that the two class names are visually identical. /0.8 and /0.80 sail through code review, past every eyeball reading the diff, and of course past a compiler that never promised to compare them.

The fix

The real fix was not correcting the zero. It was to stop writing raw color literals in markup at all. Tailwind v4 makes this easy: define the color once as a theme token in @theme and let the framework generate one canonical utility for it:

@theme {
  --color-ink-soft: oklch(0.97 0.01 260 / 0.8);
}

Then every call site points at the token:

<h2 class="text-ink-soft">
  A subtitle with exactly one spelling
</h2>

text-ink-soft cannot fork into variant spellings. The literal lives in exactly one place, and the token is canonical by construction: there is no trailing zero to disagree between what you wrote and what got generated.

While migrating, I grepped for the duplicated literal to catch every call site: the two spellings above, plus a couple of cousins with a slightly shifted hue that were clearly meant to be the same color. All of them became the token.

And one new habit: when a style goes silently missing in production while the build is green, my first suspect is now a scan or canonicalization mismatch, not the cascade.

The takeaway

Arbitrary values are strings, not values. The same color spelled two ways is two different classes, and the compiler will never warn you, because unmatched utilities fail silently by design. Theme tokens delete this entire class of problem: one literal, one place, one spelling. And when a style ghosts, grep for near-duplicate arbitrary values before you blame specificity. Same color, different string. The browser does not do math on your class names.