tailwindlabs/tailwindcss
Utilities and the design system
Every Tailwind utility (flex, bg-red-500, pt-4, rotate-90, tab-2, …) is a registered entry in a Utilities registry on the runtime DesignSystem. This page covers how that registry works.
The DesignSystem
packages/tailwindcss/src/design-system.ts defines the DesignSystem type — the runtime context every utility, variant, plugin, and codemod reads from. Built by buildDesignSystem(theme, utilitiesSrc?):
type DesignSystem = {
theme: Theme
utilities: Utilities
variants: Variants
invalidCandidates: Set<string>
important: boolean
// ...
parseCandidate(s: string): readonly Candidate[]
parseVariant(s: string): Variant | null
compileAstNodes(c: Candidate, flags?: CompileAstFlags): ...
printCandidate(c: Candidate): string
// ...
getVariantOrder(): Map<Variant, number>
resolveThemeValue(path: string, forceInline?: boolean): string | undefined
// ...
canonicalizeCandidates(cs: string[], opts?: CanonicalizeOptions): string[]
candidatesToCss(classes: string[]): (string | null)[]
candidatesToAst(classes: string[]): AstNode[][]
storage: Record<symbol, unknown>
}The storage field is a generic key-value store keyed on symbols — used by plugins to hold cross-call state without polluting the public API.
A DefaultMap (packages/tailwindcss/src/utils/default-map.ts) memoizes parseCandidate, parseVariant, and compileAstNodes so repeated build calls reuse parsing work.
The Utilities registry
packages/tailwindcss/src/utilities.ts defines Utilities, a multimap from utility-root name to Utility[]. Each entry has:
type Utility = {
kind: 'static' | 'functional';
compileFn: (candidate: Candidate) => AstNode[] | null | undefined;
options?: { types?: DataType[] };
};static— the candidate must match exactly (no value, no modifier).functional— the candidate has a value and/or modifier, e.g.bg-red-500,bg-red-500/50,bg-[rgb(0,0,0)].
compileFn(candidate):
- Returns AST nodes when the candidate produces CSS.
- Returns
nullwhen the input is invalid for this utility (a fallback utility may still try). - Returns
undefinedwhen the utility "does not apply" (e.g. wrong kind) and the next registered utility should run.
Multiple utilities can register under the same name. They run in order; the first one that returns nodes wins. If none do, "fallback" utilities (those that declare types: ['any']) are tried in a second pass. This is how arbitrary-value utilities coexist with named-value utilities.
How a candidate becomes CSS
compileAstNodes(candidate, designSystem, flags) (in packages/tailwindcss/src/compile.ts) is the dispatch point:
graph TD
A[Candidate] --> B[utilities.get candidate.root]
B --> C{Match?}
C -->|none| D[return]
C -->|some matched| E[Build base AST nodes]
E --> F[For each variant in candidate.variants:<br/>applyVariant]
F --> G[Compute property sort]
G --> H[Wrap in selector rule]
H --> I[Mark important if needed]
I --> J[AST nodes returned]After every candidate has been compiled, compileCandidates sorts the rules by:
- Variant bit-mask (so
md:always groups,dark:always groups, etc.). - Lowest property index in
GLOBAL_PROPERTY_ORDER(inpackages/tailwindcss/src/property-order.ts). - Most-properties-first (so e.g.
bordercomes beforeborder-tin tied cases). - Alphabetical candidate string.
This is what makes Tailwind's output deterministic regardless of class order in HTML.
Property order
packages/tailwindcss/src/property-order.ts exports GLOBAL_PROPERTY_ORDER, a hand-maintained array of CSS properties in the order utilities should appear in output. Every property in every built-in utility is in this list. New utilities should add their property here so they sort correctly.
The --tw-sort declaration is a backdoor for utilities that need to override their natural sort position — used by a handful of compound utilities like bg-gradient-*.
The Theme class
packages/tailwindcss/src/theme.ts defines Theme, the keyed token store. Tokens have a namespace (--color-*, --spacing, --font-*) and a value. They can be marked with ThemeOptions:
export const enum ThemeOptions {
NONE = 0,
REFERENCE = 1 << 0, // Don't emit on :root, just expose to utilities
INLINE = 1 << 1, // Inline the value into utility output instead of var()
DEFAULT = 1 << 2, // Marked as default
STATIC = 1 << 3, // Always emit on :root, even if unused
}Most tokens emit as CSS custom properties on :root only when at least one utility consumed them — the markUsedVariable mechanism. STATIC overrides that for tokens you want to emit unconditionally.
Theme.resolve(modifier, paths, options) is the lookup utilities use:
pathsis a list of token namespaces to search (--color-red-500, then--background-color-red-500, etc.).modifieris the optional opacity/percentage modifier.optionstoggles inline-vs-var()and reference-vs-actual.
packages/tailwindcss/src/css-functions.ts implements the theme(...) and --spacing(...) CSS functions, which call into the same resolver.
How utilities consume theme
A typical functional utility:
utilities.functional(
'bg',
(candidate) => {
// pulls --color-red-500 etc.
let value = theme.resolve(candidate.modifier ?? null, [
`--color-${candidate.value}`,
]);
if (!value) return null;
return [decl('background-color', value)];
},
{ types: ['color'] }
);Most of utilities.ts follows this shape. It's repetitive on purpose — every section of the file handles one CSS property family.
Default utilities and the theme.css file
The package also ships static CSS files that load the default theme. Sources:
packages/tailwindcss/theme.css— ~700 lines of@themedeclarations covering colors, spacing, typography, breakpoints, etc.packages/tailwindcss/preflight.css— the global reset.packages/tailwindcss/utilities.css— empty marker for the@layer utilitiesinjection point.packages/tailwindcss/index.css— top-level@importaggregator.
The @import "tailwindcss" shorthand resolves to index.css.
Custom utilities (@utility)
A user can declare a utility purely in CSS:
@utility tab-1 {
tab-size: 1;
}
@utility tab-* {
tab-size: --value(integer);
}createCssUtility(node) in utilities.ts parses these and registers them on the design system. Functional @utility declarations use --value(...) and --modifier(...) placeholders that the engine expands when matching candidates.
A recent CHANGELOG entry: "Allow multiple @utility definitions with the same name but different value types" (PR #19777).
Plugin-registered utilities
The v3 plugin API (addUtilities, matchUtilities) is implemented in packages/tailwindcss/src/compat/plugin-api.ts. Both ultimately call Utilities.functional(...) or Utilities.static(...). v3 plugins keep working but the data ends up in the same registry as v4-native utilities.
Adding or modifying a built-in utility
- Open
packages/tailwindcss/src/utilities.tsand find the section for the relevant CSS property. - Add or modify the registration. Match the surrounding style.
- If a new CSS property is involved, add it to
GLOBAL_PROPERTY_ORDERinproperty-order.ts. - Add tests in
utilities.test.tsnear the existing tests for the same property family. - Update
theme.cssif the new utility reads from a new theme namespace. - Add a CHANGELOG entry.
For variant-related concerns see Variants. For the candidate parsing layer see Scanning and extraction.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.