Abdulkader Safi

The design tokens were correct, and no block was reading them

Building a demo site with a teal brand, I set the primary colour token and the hero button stayed black. The tokens shipped in v0.2.0, the editor preview and the public page both read them, and the two loudest elements in the block library had the colour hardcoded the whole time.

3 min read

Share
A teal call to action panel on a page, next to the black one it replaced.

Design tokens went into Atelier eight releases ago. Colour, font, spacing and width, emitted as CSS custom properties into the public page and the editor preview from the same layout, so the two cannot drift. A site overrides one key in config and does not restate the rest.

Building a demo site for v0.5.0 was the first time I actually used them for a brand.

'tokens' => [
    'color' => [
        'primary' => '#0d9488',
        'on-primary' => '#ffffff',
        'tint' => '#f0fdfa',
    ],
],

Reload. Section backgrounds turned teal, because the shared background control resolves a token reference. The hero button stayed black. The call to action panel stayed black.

What was actually in the views

<a href="..." class="rounded-md px-5 py-3 bg-neutral-900 text-white hover:bg-neutral-700">

Nine block views, and the only thing reading --atelier-color-primary was the shared control that lets a client pick a section background. The primary colour token existed, was documented, was emitted into the page, and coloured nothing anybody looks at.

That is a specific kind of bug. Nothing is broken, no test fails, the feature works exactly as built. It is only wrong against its own purpose, which you can only see by trying to use it for the thing it was built for.

The fix, and why it is inline

<a href="..."
   class="rounded-lg px-5 py-3 text-sm font-semibold transition hover:opacity-90"
   style="background:var(--atelier-color-primary);color:var(--atelier-color-on-primary)">

Inline style, not a class. There is a rule in this project about that, written when the shared controls were built: a class written in PHP is a class Tailwind never scans. The package's block views are scanned by the consuming app, so classes in a view are fine, but a colour composed at runtime cannot become a class name that exists. Custom properties have no such problem, which is most of the reason the tokens exist.

The image variant of the hero keeps its white button. A brand colour on top of an arbitrary photograph is a coin toss, and white on a dark overlay is not.

The part that makes it a breaking change

Every site that already sets color.primary now has a different hero button and a different call to action panel. Under 1.0.0 a minor bump may carry that, and the changelog says so, but it still needs the sentence that tells someone how to get the old look back:

If you were relying on the black panel, set color.primary to #171717 and nothing moves.

That is the whole revert: the previous hardcoded value, as a token. Which is a decent sign the change is the right way round.

Why the demo found it and eight releases did not

The example app existed before this. It had pages, blocks, both languages, structured data and a passing test suite. What it did not have was a brand. Everything in it was neutral grey on white, which is exactly the palette the hardcoded values happened to be.

A demo built to prove a feature works will use the defaults. A demo built to look like a real client site sets a colour, and finds out which parts of the system were only pretending to be configurable.

Where it stands

Shipped in v0.5.0. The hero button and the call to action panel read color.primary and color.on-primary, in the editor preview as well as on the public page, because both get the same custom properties from the same layout.

Eight releases between shipping a token and having one block honour it. The tokens were not wrong. Nothing had asked them a question yet.

Last updated 13 Sep 2026 · filed under Laravel, blade, tailwindcss, filamentphp, plugin, ui, ux

Every entry on this project

28 build notes, in order

Including the ones where nothing worked. You are on part 22.

Read the series →

FAQ

Frequently asked questions

Why do design tokens end up unused in a component library?

Because components are usually written before anybody rebrands anything, and the author has a concrete colour in mind while writing markup. The token gets added later, wired into whatever surface prompted it, and the existing components keep their literal values because nothing fails. The way to catch it is to build something in a palette that is nothing like the default, which forces every hardcoded value to announce itself. A grep for colour literals in the component views is the cheap version of the same check.

Utility classes or inline custom properties for themeable colours?

Custom properties, wherever the value comes from configuration rather than from the markup. Utility-first CSS frameworks generate classes by scanning source files for literal class names, so a class name composed at runtime is a class that never gets generated, and the styling silently does nothing. A CSS custom property is resolved by the browser, so it works no matter where the value came from. Keep layout and spacing as utilities and let colour come from the property, which also gives you theming for free.

Is changing a component's default colour a breaking change?

Treat it as one in the changelog even when nothing errors. Anyone who accepted the old default is going to see a visual difference they did not ask for, and visual regressions are expensive to trace because they do not show up in a test run or a log. Say what changed, say who is affected, and give the exact one-line revert. If the revert is just setting a configuration value to the old literal, that is a good sign the change made the system more configurable rather than less predictable.

What kind of demo application actually finds bugs?

One built the way a client project would be built, not one built to exercise features. A demo that adds one of each component with default content proves the components render. A demo with a brand, real copy, a navigation menu and a form finds the places where the system is only theoretically configurable, plus the ordering and integration problems that only appear when features are used together. It costs a day and it tends to find things a feature-by-feature test suite structurally cannot.

Written by

Abdulkader Safi

Software Engineer

Lead engineer at DSRPT, from Lebanon and based in Kuwait. I write about the tools and bugs from real client work, with the numbers I measured.

About me → GitHub LinkedIn

Need this kind of work done on your project?

Start a project →

Keep reading

More from Filament Atelier

All 28 entries →
  • A version number changing from 0.5.0 to 1.0.0 beside a list of deferred items.
    Filament Atelier

    · part 28 of 28

    Tagging 1.0.0 with four features missing, on purpose

    The gate list had thirteen items. Six went in, seven did not, and the tag went out anyway. What the number promises is that the API stops moving, not that the feature list is finished, and conflating those two is how packages sit at 0.x for three years.

  • The same block of JavaScript appearing in three different Blade layout files.
    Filament Atelier

    · part 26 of 28

    A script in three layouts is a contract nobody signed

    The editor's preview needed a few lines of JavaScript in the page it renders. I put them in the shipped layout, then copied them into two more. Anyone writing their own layout had to copy them too, and missing them broke half the editor with no error at all.