tailwind-engineer
Apply utility-first discipline. Reach for design tokens before
arbitrary values, compose with @apply only where it earns its
keep, and reject inline style= drift. The skill is the how
for any Tailwind-stack screen; pair with
existing-ui-audit for the what
already exists and fe-design for the
why.
When to use
- Writing or reviewing Tailwind classes in Blade, Livewire, or React
components.
- A diff introduces inline
style= for dynamic values, hex codes
not in tailwind.config, or !important.
- Class lists balloon past ~12 utilities and the component is
hard to read or duplicate.
- German triggers: "stile mit Tailwind", "design tokens nutzen",
"warum nicht inline?".
Do NOT use when:
- The stack is not Tailwind (vanilla CSS, CSS-in-JS, MUI) — skip.
- The question is component shape, not styling — route to
ui-component-architect.
- An accessibility issue is the symptom (focus ring, contrast, hit
area) — route to
accessibility-auditor.
Procedure
1. Resolve to design tokens first
Inspect tailwind.config.{js,ts} (or the equivalent @theme block)
and identify the configured tokens. Map every requested colour,
radius, spacing, shadow, font-size to a configured token. If the design hands you #3B82F6, use
bg-blue-500 (or the project's named token). Arbitrary values
(bg-[#3B82F6], mt-[17px]) are a smell — accept only with a
one-line comment naming the design source.
That paragraph is written for greenfield, and artifact-bound work inverts
it. When a provided finished design is the spec
(design-fidelity), #3B82F6 is a decision
somebody already made and bg-blue-500 is a guess about it. Three obligations
follow, and the third is the one this skill used to get wrong:
- Do not snap. Replacing the artifact's value with the nearest configured
token is the deviation the rule's Iron Law forbids — "NEVER SWAP … SPACING,
OR COLOUR" without explicit confirmation. An artifact-derived exact value is
not a smell here: it is the spec, and the source-naming comment the
paragraph above asks for is what records it.
- Translate once, not per call site. Where the artifact's literal genuinely
needs to live in the project's system, add one named project token
carrying that exact value and use it everywhere — one token the project owns
beats N approximations of the same colour. That is a token duty, not a
deviation: the value does not change.
- Reconcile as a proposal, never autonomously. Report each value's
distance to the nearest project token on the
Reconciled: line below, and
let the human decide. Writing a different project value because the
difference looks small is exactly the unconfirmed swap (1) rules out — the
mode table at design-fidelity.md:117-121 grants no visibility exemption,
and structural grants only the filling of a spec that is genuinely
silent, which an artifact stating #3B82F6 is not.
Why not "just approximate within tolerance", which is the eventual intent:
that needs a tolerance to exist. There is none — no reconcile, tolerance,
approximat or nearest in the rule or its guideline — and both the threshold
and whether approximation is autonomous by default are owner decisions, tracked
as blocker: approximation-tolerance and blocker: fidelity-default-flip on
road-to-design-intent-conformance. Until they land, a skill granting that
autonomy would be re-writing the rule from underneath it.
Structure, controls, grid and breakpoints are never this skill's to adjust;
those stay 1:1 with the artifact and belong to the rule, not to a
utility-class decision. Icons are out of scope here as well, and this skill
asserts no obligation about them — the icon system belongs to
icon-consistency. An earlier revision of
this paragraph claimed icons "stay 1:1 with the artifact" with no citation of
any kind, while putting them outside its own scope in the same sentence; the
claim is withdrawn rather than defended, because a clause that reached the tree
first does not thereby earn an evidentiary bar for its own reversal.
Token authoring (DTCG 3-layer model, CSS-var/Tailwind generation) lives
in design-tokens; its
tokens.ts validate --dir <path> is the single token-discipline
linter — the mechanical check behind this rule (no hardcoded hex / px /
rem outside the token files). Greenfield Tailwind config: bundled
scripts/tailwind_config_gen.ts (Apache-2.0-derived, pure templating)
scaffolds tailwind.config.{js,ts} per framework.
2. Compose, don't inline
Inline style="..." is allowed only for runtime-computed values
the build cannot know (server-pushed colour, animated transform
target). Static values inline are a regression — replace with a
utility, an arbitrary value, or a token extension.
3. Order classes for scan-ability
Group by axis: layout → box-model → typography → colour → state
→ responsive. Most projects pin this with prettier-plugin-tailwindcss;
if the plugin is configured, run it; if not, follow the order
manually. Reviewer should read intent in one pass.
4. Extract only when duplicated ≥ 3 times
The first two repetitions of a utility-class string are noise; the third
is a pattern (the utility-class row of the per-class canon,
abstraction-thresholds).
Extract via:
| Mechanism |
When |
| Component (Blade/Livewire/React) |
Different content, same shell |
| Class string constant |
Same shell, different consumers in same file |
@apply in CSS |
Cross-file shared visual primitive (button, badge) |
| Tailwind plugin |
Tokens or variants, not classes |
@apply for a one-off is a regression — keep utilities inline
until the third use earns extraction.
5. Responsive + dark + state in that order
Class order within an axis: base → sm: → md: → lg: → xl: →
dark: → state (hover:, focus:, disabled:, aria-*:).
Mixing the order makes diffs noisy. State on top of dark on top
of responsive matches Tailwind's cascade and reads top-down.
Output format
When reviewing or proposing styles, return:
- Token map — every colour, spacing, radius, shadow, font-size mapped
to its configured token; arbitrary values flagged with the design
source they cite. Artifact-bound work adds a Reconciled block here:
one line per value, as
<artifact value> → <kept, or the token that carries it exactly> (<distance to the nearest project token>). This is
the destination "report the distance" means; without it the obligation
has nowhere to land and is unobservable in the deliverable.
- Class list — ordered (layout → box-model → typography → colour →
state → responsive); inline-style use justified per element.
- Extraction + risk call-out — component / constant /
@apply / none
with reason; risks named (arbitrary values, !important, dark-mode
gaps, non-token references).
Concrete shape:
Element: <selector or component name>
Token map: <colour/spacing/etc → config token>
Reconciled: <artifact value → kept or exact-carrying token, + distance>
Class list: <ordered classes>
Inline style: <only if runtime-computed; else "none">
Extraction: <component | constant | @apply | none — reason>
Risks: <arbitrary values, !important, dark-mode gaps>
Gotcha
space-x-* / space-y-* collide with flex-wrap and RTL — use
gap-* on the flex/grid parent unless the design demands otherwise.
dark: variants need a token map in both modes; one-sided dark
styling is half a feature.
- Arbitrary values (
mt-[17px]) survive Tailwind upgrades but
break the design system; they accumulate silently. Artifact-bound is the
exception, not a loophole: a value the provided design specifies is not
accumulation, and step 1's three obligations decide it. An arbitrary value
nobody can trace to a source is still the smell this bullet is about.
- The carrier is CSS or a utility class; the value is the design's. Static
presentation belongs in CSS / tokens / classes —
style= is for what only
the runtime knows (a computed width, a live transform, a measured offset).
Porting an artifact's inline style= into classes is therefore expected and
is not a deviation, as long as the resolved value comes out identical.
Changing the carrier is free; changing the number is not.
@apply inside component CSS interacts with PurgeCSS — keep it
in files Tailwind scans, not in vendor CSS.
- Anti-AI-slop catalog. The bullets below are the Tailwind-specific
manifestations of the stack-agnostic patterns in
docs/guidelines/design-antipatterns.md
(C1 gradients, T7 fonts, L1/L2 layout, V1 side-stripe). Pull the catalog
for the full list; the objective subset (contrast, font-size floor,
reduced-motion) is validated via accessibility-auditor's checklist —
cite its verdict rather than eyeballing.
- Anti-AI-slop: gradients. Unless audit-pinned or brief-explicit,
avoid the default purple-to-blue / cyan-to-pink gradients on white —
they read as auto-generated (catalog C1/C2). Reach for a single accent
from the token map, or a duotone built from configured tokens.
- Anti-AI-slop: typography. Unless audit-pinned, avoid surfacing
the system stack (
font-sans fallback to Arial / Helvetica / Inter
via system defaults) as the visible body face. If tailwind.config
pins a font family, use it; if not, treat the missing token as a
gap to flag, not a license to ship the OS default.
- Anti-AI-slop: layout. Unless audit-pinned, the centered hero +
3-column features + CTA stack is the AI-template tell. Break the
grid intentionally (asymmetric column split, overlap, diagonal
flow) when the brief allows; cite the design brief's
aesthetic:
line if fe-design's aesthetic-direction section produced one.
Taste Dials
When DESIGN.md declares ## Taste Dials, honour them: Variance → layout-family spread + asymmetry tolerance; Motion → animation budget + reduced-motion posture; Density → spacing scale + information-per-viewport. Absent → follow the design brief's inferred dials.
Security constraints
scripts/tailwind_config_gen.ts is the only shipped script.
- What it may touch — exactly one file:
--output PATH, or
tailwind.config.ts / .js in the current working directory when no
--output is given. It reads nothing from the project.
- What it must never do — write anywhere other than that one path,
reach the network, or spawn a subprocess. It does none of these.
- Default invocation — mutating, and stated here because it is a
real exception rather than a comfortable one: a bare invocation writes
tailwind.config.ts into cwd, overwriting an existing config without
prompting. --validate-only is the read-only path — it prints the
generated config to stdout and writes nothing. Run --validate-only
first, and pass --output when the target is not the cwd default.
This default contradicts skill-writing's "never mutate on a bare
invocation" line; it is recorded rather than silently tolerated, and
changing the script's default is a caller-visible behaviour change that
belongs in its own change, not here.
- Outbound — nothing. No network access.
Do NOT
- Do NOT run
tailwind_config_gen bare in a project that already has a
tailwind.config.* — it overwrites without asking. Use
--validate-only first, then --output at the intended path.
- Do NOT add
!important to win a specificity fight; restructure
the cascade or extract the conflicting style.
- Do NOT introduce a new colour outside
tailwind.config without
also adding the token; one-off hex codes drift the system.
- Do NOT use
@apply to avoid utility verbosity inside a single
component — extract the component instead.
- Do NOT ship
style= for static values; that is a CSS regression
the linter will not catch.
1---2name: tailwind-engineer3description: Writing or reviewing Tailwind CSS — utility-first, design tokens, no inline-style drift, responsive variants, dark mode — 'style this' or 'mach das hübsch'. Pairs with react-shadcn-ui.4---56# tailwind-engineer78> Apply utility-first discipline. Reach for design tokens before9> arbitrary values, compose with `@apply` only where it earns its10> keep, and reject inline `style=` drift. The skill is the **how**11> for any Tailwind-stack screen; pair with12> [`existing-ui-audit`](../existing-ui-audit/SKILL.md) for the **what13> already exists** and [`fe-design`](../fe-design/SKILL.md) for the14> **why**.1516## When to use1718- Writing or reviewing Tailwind classes in Blade, Livewire, or React19 components.20- A diff introduces inline `style=` for dynamic values, hex codes21 not in `tailwind.config`, or `!important`.22- Class lists balloon past ~12 utilities and the component is23 hard to read or duplicate.24- German triggers: "stile mit Tailwind", "design tokens nutzen",25 "warum nicht inline?".2627Do NOT use when:2829- The stack is not Tailwind (vanilla CSS, CSS-in-JS, MUI) — skip.30- The question is component shape, not styling — route to31 [`ui-component-architect`](../ui-component-architect/SKILL.md).32- An accessibility issue is the symptom (focus ring, contrast, hit33 area) — route to [`accessibility-auditor`](../accessibility-auditor/SKILL.md).3435## Procedure3637### 1. Resolve to design tokens first3839Inspect `tailwind.config.{js,ts}` (or the equivalent `@theme` block)40and identify the configured tokens. Map every requested colour,41radius, spacing, shadow, font-size to a configured token. If the design hands you `#3B82F6`, use42`bg-blue-500` (or the project's named token). Arbitrary values43(`bg-[#3B82F6]`, `mt-[17px]`) are a smell — accept only with a44one-line comment naming the design source.4546**That paragraph is written for greenfield, and artifact-bound work inverts47it.** When a provided finished design is the spec48([`design-fidelity`](../../rules/design-fidelity.md)), `#3B82F6` is a decision49somebody already made and `bg-blue-500` is a guess about it. Three obligations50follow, and the third is the one this skill used to get wrong:51521. **Do not snap.** Replacing the artifact's value with the nearest configured53 token is the deviation the rule's Iron Law forbids — *"NEVER SWAP … SPACING,54 OR COLOUR"* without explicit confirmation. An artifact-derived exact value is55 not a smell here: it *is* the spec, and the source-naming comment the56 paragraph above asks for is what records it.572. **Translate once, not per call site.** Where the artifact's literal genuinely58 needs to live in the project's system, add **one** named project token59 carrying that exact value and use it everywhere — one token the project owns60 beats N approximations of the same colour. That is a token duty, not a61 deviation: the value does not change.623. **Reconcile as a proposal, never autonomously.** Report each value's63 distance to the nearest project token on the `Reconciled:` line below, and64 let the human decide. Writing a *different* project value because the65 difference looks small is exactly the unconfirmed swap (1) rules out — the66 mode table at `design-fidelity.md:117-121` grants no visibility exemption,67 and `structural` grants only the filling of a spec that is genuinely68 **silent**, which an artifact stating `#3B82F6` is not.6970**Why not "just approximate within tolerance", which is the eventual intent:**71that needs a tolerance to exist. There is none — no `reconcile`, `tolerance`,72`approximat` or `nearest` in the rule or its guideline — and both the threshold73and whether approximation is autonomous by default are owner decisions, tracked74as `blocker: approximation-tolerance` and `blocker: fidelity-default-flip` on75`road-to-design-intent-conformance`. Until they land, a skill granting that76autonomy would be re-writing the rule from underneath it.7778Structure, controls, grid and breakpoints are never this skill's to adjust;79those stay 1:1 with the artifact and belong to the rule, not to a80utility-class decision. **Icons are out of scope here as well, and this skill81asserts no obligation about them** — the icon system belongs to82[`icon-consistency`](../../rules/icon-consistency.md). An earlier revision of83this paragraph claimed icons "stay 1:1 with the artifact" with no citation of84any kind, while putting them outside its own scope in the same sentence; the85claim is withdrawn rather than defended, because a clause that reached the tree86first does not thereby earn an evidentiary bar for its own reversal.8788Token authoring (DTCG 3-layer model, CSS-var/Tailwind generation) lives89in [`design-tokens`](../design-tokens/SKILL.md); its90`tokens.ts validate --dir <path>` is the **single token-discipline91linter** — the mechanical check behind this rule (no hardcoded hex / px /92rem outside the token files). Greenfield Tailwind config: bundled93`scripts/tailwind_config_gen.ts` (Apache-2.0-derived, pure templating)94scaffolds `tailwind.config.{js,ts}` per framework.9596### 2. Compose, don't inline9798Inline `style="..."` is allowed only for **runtime-computed values**99the build cannot know (server-pushed colour, animated transform100target). Static values inline are a regression — replace with a101utility, an arbitrary value, or a token extension.102103### 3. Order classes for scan-ability104105Group by axis: layout → box-model → typography → colour → state106→ responsive. Most projects pin this with `prettier-plugin-tailwindcss`;107if the plugin is configured, run it; if not, follow the order108manually. Reviewer should read intent in one pass.109110### 4. Extract only when duplicated ≥ 3 times111112The first two repetitions of a **utility-class string** are noise; the third113is a pattern (the utility-class row of the per-class canon,114[`abstraction-thresholds`](../../../docs/guidelines/abstraction-thresholds.md)).115Extract via:116117| Mechanism | When |118|---|---|119| Component (Blade/Livewire/React) | Different content, same shell |120| Class string constant | Same shell, different consumers in same file |121| `@apply` in CSS | Cross-file shared visual primitive (button, badge) |122| Tailwind plugin | Tokens or variants, not classes |123124`@apply` for a one-off is a regression — keep utilities inline125until the third use earns extraction.126127### 5. Responsive + dark + state in that order128129Class order within an axis: base → `sm:` → `md:` → `lg:` → `xl:` →130`dark:` → state (`hover:`, `focus:`, `disabled:`, `aria-*:`).131Mixing the order makes diffs noisy. State on top of dark on top132of responsive matches Tailwind's cascade and reads top-down.133134## Output format135136When reviewing or proposing styles, return:1371381. Token map — every colour, spacing, radius, shadow, font-size mapped139 to its configured token; arbitrary values flagged with the design140 source they cite. Artifact-bound work adds a **Reconciled** block here:141 one line per value, as `<artifact value> → <kept, or the token that142 carries it exactly> (<distance to the nearest project token>)`. This is143 the destination "report the distance" means; without it the obligation144 has nowhere to land and is unobservable in the deliverable.1452. Class list — ordered (layout → box-model → typography → colour →146 state → responsive); inline-style use justified per element.1473. Extraction + risk call-out — component / constant / `@apply` / none148 with reason; risks named (arbitrary values, `!important`, dark-mode149 gaps, non-token references).150151Concrete shape:152153```154Element: <selector or component name>155Token map: <colour/spacing/etc → config token>156Reconciled: <artifact value → kept or exact-carrying token, + distance>157Class list: <ordered classes>158Inline style: <only if runtime-computed; else "none">159Extraction: <component | constant | @apply | none — reason>160Risks: <arbitrary values, !important, dark-mode gaps>161```162163## Gotcha164165- `space-x-*` / `space-y-*` collide with `flex-wrap` and RTL — use166 `gap-*` on the flex/grid parent unless the design demands otherwise.167- `dark:` variants need a token map in both modes; one-sided dark168 styling is half a feature.169- Arbitrary values (`mt-[17px]`) survive Tailwind upgrades but170 break the design system; they accumulate silently. **Artifact-bound is the171 exception, not a loophole:** a value the provided design specifies is not172 accumulation, and step 1's three obligations decide it. An arbitrary value173 nobody can trace to a source is still the smell this bullet is about.174- **The carrier is CSS or a utility class; the value is the design's.** Static175 presentation belongs in CSS / tokens / classes — `style=` is for what only176 the runtime knows (a computed width, a live transform, a measured offset).177 Porting an artifact's inline `style=` into classes is therefore expected and178 is not a deviation, **as long as the resolved value comes out identical**.179 Changing the carrier is free; changing the number is not.180- `@apply` inside component CSS interacts with PurgeCSS — keep it181 in files Tailwind scans, not in vendor CSS.182- **Anti-AI-slop catalog.** The bullets below are the Tailwind-specific183 manifestations of the stack-agnostic patterns in184 [`docs/guidelines/design-antipatterns.md`](../../../docs/guidelines/design-antipatterns.md)185 (C1 gradients, T7 fonts, L1/L2 layout, V1 side-stripe). Pull the catalog186 for the full list; the objective subset (contrast, font-size floor,187 reduced-motion) is validated via `accessibility-auditor`'s checklist —188 cite its verdict rather than eyeballing.189- **Anti-AI-slop: gradients.** Unless audit-pinned or brief-explicit,190 avoid the default purple-to-blue / cyan-to-pink gradients on white —191 they read as auto-generated (catalog C1/C2). Reach for a single accent192 from the token map, or a duotone built from configured tokens.193- **Anti-AI-slop: typography.** Unless audit-pinned, avoid surfacing194 the system stack (`font-sans` fallback to Arial / Helvetica / Inter195 via system defaults) as the *visible* body face. If `tailwind.config`196 pins a font family, use it; if not, treat the missing token as a197 gap to flag, not a license to ship the OS default.198- **Anti-AI-slop: layout.** Unless audit-pinned, the centered hero +199 3-column features + CTA stack is the AI-template tell. Break the200 grid intentionally (asymmetric column split, overlap, diagonal201 flow) when the brief allows; cite the design brief's `aesthetic:`202 line if `fe-design`'s aesthetic-direction section produced one.203204## Taste Dials205206When `DESIGN.md` declares `## Taste Dials`, honour them: Variance → layout-family spread + asymmetry tolerance; Motion → animation budget + reduced-motion posture; Density → spacing scale + information-per-viewport. Absent → follow the design brief's inferred dials.207208## Security constraints209210`scripts/tailwind_config_gen.ts` is the only shipped script.211212- **What it may touch** — exactly one file: `--output PATH`, or213 `tailwind.config.ts` / `.js` in the current working directory when no214 `--output` is given. It reads nothing from the project.215- **What it must never do** — write anywhere other than that one path,216 reach the network, or spawn a subprocess. It does none of these.217- **Default invocation** — **mutating**, and stated here because it is a218 real exception rather than a comfortable one: a bare invocation writes219 `tailwind.config.ts` into `cwd`, overwriting an existing config without220 prompting. `--validate-only` is the read-only path — it prints the221 generated config to stdout and writes nothing. Run `--validate-only`222 first, and pass `--output` when the target is not the cwd default.223 This default contradicts `skill-writing`'s "never mutate on a bare224 invocation" line; it is recorded rather than silently tolerated, and225 changing the script's default is a caller-visible behaviour change that226 belongs in its own change, not here.227- **Outbound** — nothing. No network access.228229## Do NOT230231- Do NOT run `tailwind_config_gen` bare in a project that already has a232 `tailwind.config.*` — it overwrites without asking. Use233 `--validate-only` first, then `--output` at the intended path.234- Do NOT add `!important` to win a specificity fight; restructure235 the cascade or extract the conflicting style.236- Do NOT introduce a new colour outside `tailwind.config` without237 also adding the token; one-off hex codes drift the system.238- Do NOT use `@apply` to avoid utility verbosity inside a single239 component — extract the component instead.240- Do NOT ship `style=` for static values; that is a CSS regression241 the linter will not catch.