Skip to content

[6.x] Generate Tailwind utilities from the design tokens - #19494

Draft
brianjhanson wants to merge 9 commits into
6.xfrom
feature/tailwind-color-tokens
Draft

[6.x] Generate Tailwind utilities from the design tokens#19494
brianjhanson wants to merge 9 commits into
6.xfrom
feature/tailwind-color-tokens

Conversation

@brianjhanson

@brianjhanson brianjhanson commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Description

Republishes Craft's design tokens into the Tailwind namespaces that generate utility classes for them — --color-* for the semantic colors, --spacing-* for the spacing scale.

Colors

Each semantic color token gets a matching set of utility classes. The token name is the utility name — drop the --c-color- prefix, and what's left is what you type:

Token Utilities
--c-color-neutral-border-quiet border-neutral-border-quiet, divide-…, ring-…
--c-color-danger-fill-normal bg-danger-fill-normal
--c-color-accent-on-loud text-accent-on-loud

The generic tokens are mapped too, which is usually what you want inside a component: bg-fill-quiet, border-border-normal, and text-on-loud paint with whatever the nearest [data-color] or variant resolved to, rather than naming a color up front.

Scope is deliberately narrow — the six semantic groups plus the generic set, 63 entries. The raw palette (--c-color-red-* and friends) stays reachable from CSS and from [data-color], it just doesn't get utility classes.

Spacing

The named steps on the spacing scale map into --spacing-*, so they work everywhere a spacing utility does — p-sm, gap-md, mt-lg, -mx-xs, size-xl, space-y-2xl.

Token Utilities
--c-spacing-sm p-sm, gap-sm, mx-sm, …
--c-spacing-2xl p-2xl, mt-2xl, …
--c-spacing-1px p-1px, gap-1px, …

The numeric scale is untouched — p-4 still resolves through Tailwind's own --spacing base. That base is deliberately not mapped: in Tailwind v4 a bare --spacing is what generates the numeric utilities, so pointing it at --c-spacing would rewrite every numeric spacing utility in the CP rather than adding to them.

Generation

packages/craftcms-ui/tailwind.css is now generated rather than hand-maintained, by a new scripts/generate-tailwind.js wired into build.js and npm run generate:colors. Colors come from src/constants/colors.data.ts, the same source colorable.css already used, so adding a semantic color there gives you its utilities for free. The shared loadColorData() helper moved to scripts/utils.js.

The mapping stays @theme inline, so utilities reference the custom properties instead of snapshotting their values — [data-theme='dark'] and [data-color] both keep working through them.

Two corrections to the old hand-written file: it declared a whole --color-brand-* block pointing at --c-color-brand-* variables that don't exist anywhere, and carried border-faint/border-subtle/border-default/border-strong aliases from the pre-rename vocabulary. Both are gone; the legacy aliases had no usages.

Also fixes class names in the codebase that never had a matching variable and were rendering as nothing:

  • text-neutral-text-quiettext-neutral-on-quiet (6 sites)
  • border-neutral-quietborder-neutral-border-quiet
  • text-danger-texttext-danger-on-normal, matching what .error-list already uses

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/uiopen Storybook

Changed components:

resources/jsopen Storybook

No changed components detected in this Storybook.

@brianjhanson brianjhanson changed the title [6.x] Generate Tailwind color utilities from the design tokens [6.x] Generate Tailwind utilities from the design tokens Aug 26, 2026
Replaces the `@theme inline` mapping with prefixed `@utility` rules. Class
names no longer repeat the token's role — the property implies it, so
`--c-color-danger-fill-normal` is `c-bg-danger-normal` rather than
`bg-danger-fill-normal`. Text takes both roles: `c-text-*` inks with the
fill color, `c-text-on-*` with the color meant to stay legible on it.

Tailwind's own `prefix()` applies to a whole build, and the CP writes plain
Tailwind everywhere, so the prefix lives in the class names instead. That also
means `tailwind.css` can no longer be imported into a layer — `@utility` is
only valid at the top level of a stylesheet.
Puts an element in a color group with a class that matches the `c-` utility
prefix, and an attribute that matches the class. The Craft 5 spellings
(`.cp-color-*`, `[data-color]`) stay on the same rule, so existing markup and
plugins keep working.

`data-theme` was the obvious name but it's already the light/dark color-scheme
attribute, which is a different axis entirely.
… shell

Sets Tailwind's `prefix(cp)`, so Craft's token utilities and stock Tailwind
land in one namespace — `cp:bg-loud` next to `cp:flex` — and anything
unprefixed reads as a legacy class from the old SCSS bundle.

The prefix is `cp` rather than `c` because `prefix(c)` renames Tailwind's
own theme variables into `--c-*`, which is Craft's token namespace; eight of
them (`--c-text-sm`, `--c-radius-sm`, `--c-font-mono`, …) would have landed
on Craft tokens of the same name holding different values.

The utilities move to a new `resources/css/utilities.css` entry — tokens,
Tailwind, and Craft's utilities, with no preflight — which both shells now
load. The legacy Twig shell renders its own document and never called
`Cp::viteScripts()`, so it had neither the utilities nor the `--c-*` tokens
its own compatibility aliases resolve against; `CpAsset` now pulls the entry
in without re-resetting the page.

`hidden` and `inline-flex` stay unprefixed: the legacy bundle defines both as
bare rules that mean more than Tailwind's (an `!important`, and alignment plus
a gap), so they are legacy classes that merely share a Tailwind name.

The colorable class follows the CP's `.cp-*` convention as `.cp-palette-*`.
The gap utilities in _main.scss are the first to go now that both shells load
the same utility stylesheet.

The scales are named differently, so the mapping shifts by one step — legacy
`--xs` aliases to `--c-spacing-sm`, not `-xs`. A name-for-name sweep would
have quietly halved the spacing at every call site:

  gap-2xs -> cp:gap-xs    gap-s -> cp:gap-md    gap-l  -> cp:gap-lg
  gap-xs  -> cp:gap-sm    gap-m -> cp:gap-lg    gap-xl -> cp:gap-xl

Verified in the built CSS that each replacement resolves to the same token the
legacy alias did.
CP code only ever runs in the CP, where this is the only Tailwind there is, so
there is no raw-Tailwind context to tell a Craft utility apart from — the
prefix was ceremony at every call site for a distinction that never comes up.
`bg-loud` now sits next to `flex`, `p-md` next to `p-4`.

Tailwind's default theme keeps its own namespaces (`--spacing`, `--text-*`,
`--radius-*`), which never overlapped Craft's `--c-*` tokens; `--color-*` is
the one they share, and Craft's palette replaces Tailwind's there as it always
has.

Where the unlayered legacy bundle defines a name Tailwind also generates
(`hidden`, `inline-flex`, `truncate`) the legacy rule still wins, which is
the behaviour that predates this branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant