Working with Tailwind
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/design/references/css/tailwind-config.md |
| Description | Not specified |
Source Content
Working with Tailwind
Tailwind is a tool for applying your design tokens fast. It is not a second design system competing with your CSS. The whole point of this file: configure Tailwind so its utilities reference the same CSS custom properties your hand-written CSS uses, then let each layer do the one job it is good at.
- Extend the config, do not fight it
- Which layer owns this style?
- Performance: content globs, safelist, @apply
- Plugins
Extend the config, do not fight it
Map your tokens into theme.extend so every utility resolves to a var(--…). The
token is the source of truth; Tailwind is just a shorthand for reading it. When the
theme flips (light, dark, brand-b) the variable changes and the utility follows — you
never swap classes per theme. Token mechanics live in
../tokens-and-theming.md; here we only wire them to Tailwind.
/* ============================================================ * tailwind.config.js — utilities read the SAME vars as our CSS * One source of truth: the :root tokens. Tailwind never invents * its own colors or spacing; it points at ours. * ============================================================ */module.exports = { theme: { extend: { colors: { // semantic only — no raw hex, no palette numbers here. fg: "var(--color-fg)", bg: "var(--color-bg)", primary: "var(--color-primary)", muted: "var(--color-muted)", border: "var(--color-border)", }, spacing: { // mirror the 8pt scale tokens, not a parallel set of numbers. 1: "var(--space-1)", // 4px 2: "var(--space-2)", // 8px 4: "var(--space-4)", // 16px 6: "var(--space-6)", // 24px }, borderRadius: { sm: "var(--radius-sm)", md: "var(--radius-md)", lg: "var(--radius-lg)", }, }, },};Now bg-primary, p-4, and rounded-md flip with the theme automatically, and
.btn { background: var(--color-primary); } in your CSS stays in perfect sync — both
read the one variable.
Tailwind v4 moves this into CSS itself with @theme. The syntax differs but the
principle is identical: tokens sit behind the utilities, and there is one source of
truth.
/* tailwind.config.js equivalent in Tailwind v4 — tokens in CSS */@theme { --color-primary: var(--color-primary); /* utility bg-primary reads this */ --spacing-4: var(--space-4); --radius-md: var(--radius-md);}Which layer owns this style?
Three places a style can live. Pick one owner per responsibility — never two for the same job (see ./bem-and-tailwind.md for the full coexistence model and the “one owner” rule).
Reach for a Tailwind utility
The default for non-semantic styling: layout, spacing, alignment, and one-off visual adjustments that do not deserve a name. Putting a card in a column with a gap, nudging one icon, setting a max-width — utilities, every time.
<!-- layout + spacing = utilities; the component class owns the rest --><article class="card flex flex-col gap-4 p-6">…</article>Reach for a BEM component class
A reusable, named, meaningful component with variants and states — its API. Also the right move when a utility pattern repeats 3+ times: name it once instead of copying the class string everywhere (this is the no-duplication rule from ./architecture.md).
/* .card is a real component with an owned look; variants live here, not in markup */.card { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius-lg);}.card--selected { border-color: var(--color-primary);} /* state owned in ONE place */Reach for hand-written CSS
When utilities express it poorly: complex selectors, pseudo-elements (::before,
::after), @media print, @container, keyframes, and intricate state. Forcing these
through utilities or long @apply chains is fighting the tool.
/* keyframes + pseudo-element: utilities can't say this cleanly */.spinner::after { content: ""; animation: spin 0.6s linear infinite;}@keyframes spin { to { transform: rotate(360deg); }}Performance: content globs, safelist, @apply
Tailwind ships only the classes it sees in your source. Point content at every file
that contains class names so unused utilities are purged and the bundle stays small.
/* content globs decide what survives the purge — be precise, not greedy */module.exports = { content: [ "./src/**/*.{html,js,ts,jsx,tsx,astro}", "./components/**/*.{html,js,ts,jsx,tsx}", ],};Keep the safelist tiny. It forces classes to ship even when the scanner cannot see
them (e.g. names built at runtime) — every entry is weight that defeats purging.
Safelist only what is genuinely dynamic, and prefer mapping dynamic values to full
class strings the scanner can see.
Use @apply sparingly. It is fine for collapsing a tiny repeated utility set into one
component class, but long @apply chains inline the utilities into every rule — that
duplicates declarations instead of sharing them, which is exactly what utilities exist
to avoid. When a rule needs many properties, write plain CSS reading the tokens.
/* OK: a couple of utilities folded into a component class */.field-label { @apply text-sm font-medium; color: var(--color-fg); /* token via CSS, not a utility */}Plugins
Plugins extend Tailwind’s vocabulary; the same token discipline applies to anything they add.
Forms reset
A forms plugin (e.g. an official forms plugin) normalizes native input, select, and
checkbox styling so your component classes start from a predictable baseline. Add it
once at the config level — do not re-reset form elements per component.
Custom utilities
You can register your own utilities, but they must map to tokens, never to raw values — a custom utility hardcoding a hex color breaks theming the same way an inline hex does. If a “utility” is really a named, reusable thing with variants, it is a component class, not a utility (see ./bem-and-tailwind.md).