Open-Source Wikis

/

Tailwind CSS

/

Features

/

Utilities and the design system

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 null when the input is invalid for this utility (a fallback utility may still try).
  • Returns undefined when 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:

  1. Variant bit-mask (so md: always groups, dark: always groups, etc.).
  2. Lowest property index in GLOBAL_PROPERTY_ORDER (in packages/tailwindcss/src/property-order.ts).
  3. Most-properties-first (so e.g. border comes before border-t in tied cases).
  4. 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:

  • paths is a list of token namespaces to search (--color-red-500, then --background-color-red-500, etc.).
  • modifier is the optional opacity/percentage modifier.
  • options toggles 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 @theme declarations covering colors, spacing, typography, breakpoints, etc.
  • packages/tailwindcss/preflight.css — the global reset.
  • packages/tailwindcss/utilities.css — empty marker for the @layer utilities injection point.
  • packages/tailwindcss/index.css — top-level @import aggregator.

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

  1. Open packages/tailwindcss/src/utilities.ts and find the section for the relevant CSS property.
  2. Add or modify the registration. Match the surrounding style.
  3. If a new CSS property is involved, add it to GLOBAL_PROPERTY_ORDER in property-order.ts.
  4. Add tests in utilities.test.ts near the existing tests for the same property family.
  5. Update theme.css if the new utility reads from a new theme namespace.
  6. 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.

Utilities and the design system – Tailwind CSS wiki | Factory