Design Tools

Design Tokens vs One-Off Colors in a Product UI

Six grays live in the checkout flow of a mid-sized e-commerce app. There is the gray of the disabled button, the gray of the placeholder text, the gray of the border on the promo code field, the gray of the "or" divider, the gray of the shipping estimate, and the gray of the hover state that nobody can quite remember defining. Three of them were picked by a designer on a Tuesday afternoon. Two came from a component library installed two years earlier. One is the browser default that survived a refactor because removing it felt risky.

A design token system would have prevented most of this. That is the sales pitch, and it is largely true. What the pitch leaves out is the bill that comes due later, in the form of a vocabulary problem that colors alone never had. The choice between tokens and one-off colors is not really a choice between order and chaos. It is a choice between two different kinds of maintenance, and most teams pick one without understanding what they are signing up for.

What a one-off color actually costs

The case against hardcoded colors is usually made in terms of consistency, and consistency is the weaker argument. A user will not notice that the promo code border is a slightly different gray from the divider. A user will notice when the disabled button and the enabled button are the same shade, but that is an accessibility failure, not a taste failure.

The real cost of one-off colors shows up in the second year. A brand refresh lands. Marketing wants the accent color shifted from a warm coral to a cooler red. Someone opens the codebase and searches for the hex value. They find it in ninety-four places. Eighty-one of those should change. Thirteen are the error state, which happens to share the old hex because a designer reused the same swatch for "brand accent" and "validation error" back when the product only had four screens.

Now the change is a judgment call at every one of those ninety-four sites. That is the price. Not inconsistency, but a migration with no clear boundary between what should move and what should stay. The hex value carries no information about intent, so intent has to be reconstructed by a human reading each usage in context. At scale, that reconstruction is the expensive part of any design change.

The token that outlives the color it names

A token fixes this by separating the name from the value. The button does not hold #E4573D. It holds a reference to a semantic token called something like color-action-primary. When the brand shifts, one definition changes and the button follows. The hex value no longer appears in the component at all.

This is genuinely better, and it is why token systems have become the default recommendation for any product with more than a handful of screens. But the same separation that makes tokens powerful is what makes them fragile in a way hex values never are. A hex value is dumb. It cannot lie. A token can.

Consider what happens when the semantic layer grows. color-action-primary exists. Then someone needs a slightly darker version for the pressed state, so color-action-primary-pressed appears. Then a lighter one for hover. Then a variant for the destructive action, which is red but not the same red as the error red, because the destructive button should feel serious and the error message should feel urgent, and those are somehow different shades of red that only two people on the team can tell apart. Then a muted variant for the disabled destructive button. Then the same set again for the dark theme.

Within eighteen months the token file has two hundred entries and nobody can say with confidence which of the four reds is correct for a confirmation dialog. The tokens have become a second codebase, and it is one that lives in a JSON file that no one fully reviews.

Naming is the part that breaks

The failure mode of token systems is almost never the technology. The tooling works. The build step resolves references correctly. The variables reach the browser. The failure mode is the naming, because naming requires a team to agree on what things mean, and meaning drifts faster than anyone expects.

A token called color-surface-raised means something specific on day one: it is the background of a card sitting on the page. On day four hundred it is the background of a modal, a dropdown, a toast, and a popover, because all of those were "raised" in some sense and nobody wanted to add a new token. The name did not change. The meaning did. Now a designer who wants to adjust card backgrounds will also adjust every modal in the product, and will discover this only after the change ships.

Hex values have exactly this problem, but the failure is louder. When someone reuses a hex, the reuse is visible in the code as a literal number, and a careful reviewer can flag it. When someone reuses a token, the reuse looks intentional. The name reads like a deliberate decision. That is the trap. Tokens make accidental coupling look like architecture.

The one-off color as a deliberate design decision

There is a case for the one-off, and it is not nostalgia. Some colors in a product genuinely appear once. The green on the success toast that fires after a password change. The amber on the banner warning about a legacy browser. The specific slate on the empty-state illustration that was commissioned from an illustrator and will never be reused.

Turning these into tokens adds names to a system that gains nothing from them. The success green will not be reused. It will not participate in a theme. It will not respond to a brand refresh, because the illustrator's file is a fixed asset. Giving it a semantic token name creates a promise of reuse that will never be kept, and the next designer who sees color-feedback-positive in the token list will assume it is the right green for their new feature, and then the green spreads to places it was never designed for.

The one-off color, sitting inline next to the single component that uses it, tells the truth: this color belongs here and nowhere else. That is honest. A token would be a small lie about the color's role.

Where the line actually falls

The useful distinction is not token versus hex. It is whether a color's meaning is shared across components or local to one. A color that appears in three or more places, or that must respond to a theme or brand change, deserves a token. A color that lives in exactly one component and will not move is a candidate for inline definition, and forcing it into the token file is a net loss.

The trouble is that the first category is easy to identify in retrospect and hard to predict in advance. A color that starts local often becomes shared. The amber warning on the legacy browser banner is local until the product adds a second warning banner six months later, and now the amber is reused, and the second use copies the hex from the first because there is no token for it. That copy is the moment the local color should have become a token, and nobody noticed it happening.

Practical teams handle this with a rule that is easier to enforce than it sounds: a second usage of a color triggers a token. The first use stays inline. The moment a hex value appears twice in the codebase, it gets promoted to a named token with a name that describes its role, and both usages adopt the token. This keeps the token file from filling with one-use entries while still catching the colors that actually spread.

The audit nobody schedules

Both approaches rot, and they rot in different directions. A one-off system drifts toward inconsistency, with near-duplicate grays multiplying in the corners. A token system drifts toward semantic overload, with single tokens accumulating meanings until no one can change them safely.

The one-off drift is visible. Someone can screenshot the product, sort the colors by frequency, and find the thirty-two grays. The token drift is invisible, because the token names still look clean. The audit that catches it is not a color audit but a usage audit: for each token, list every component that references it, and read that list out loud. When color-surface-raised turns out to be used by the card, the modal, the dropdown, the toast, the popover, and the sidebar, the name is a fiction and the token needs splitting.

That list is uncomfortable to read. It means work. It means admitting that a token the team has trusted for two years is doing six different jobs under one name. But the exercise is cheap compared to the alternative, which is discovering the overload the hard way, in a pull request that adjusts the card background and silently restyles every modal in the product on the same afternoon.

A palette of eleven

A design system built by a small team for a single product, with eleven colors in active use and forty screens, does not need two hundred tokens. It needs eleven names, a rule that new colors require a conversation, and a habit of checking whether any token has quietly acquired a second job.

The six grays in the checkout flow are not a failure of token discipline. They are what happens when a product grows faster than the vocabulary that describes it. The fix is not a full migration to a semantic token system. The fix is finding the two grays that are genuinely the same and giving them one name, then leaving the other four alone until a second component needs them.

The leftover hex values can stay in the codebase a while longer. They are not ashamed of what they are.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button