Skip to content

Working with Tailwind

FieldValue
TypeSkill Resource
Source~/.copilot/skills/design/references/css/tailwind-config.md
DescriptionNot 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

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).