tailwindlabs/tailwindcss
Patterns and conventions
Style
Prettier configuration is embedded in the root package.json:
{
"prettier": {
"semi": false,
"singleQuote": true,
"printWidth": 100,
"plugins": ["prettier-plugin-organize-imports"]
}
}prettier-plugin-organize-imports reorganizes imports automatically. Don't fight it — your editor's "format on save" should be enabled.
For integrations/, an additional Prettier plugin (prettier-plugin-embed) formats embedded CSS/JS strings inside test fixtures.
Rust code uses default rustfmt plus the project rules in crates/node/rustfmt.toml.
Module shape
Most TypeScript files in the compiler follow one of three shapes:
- Function modules. A single primary exported function plus its helpers (e.g.
apply.tsexportssubstituteAtApply;at-import.tsexportssubstituteAtImports;compile.tsexportscompileCandidates). - Type/data modules. Pure data exports (
property-order.ts) or interfaces only (types.ts). - Class-shaped modules. A few places use a class because they hold state across calls —
Theme(theme.ts),Instrumentation(@tailwindcss/node).
The entry point of every package is src/index.ts. The compiler additionally exports index.cts and index.bench.ts for CommonJS consumers and benchmarks respectively.
AST conventions
The compiler's AST is defined in packages/tailwindcss/src/ast.ts:
type AstNode =
| StyleRule // selector + nodes
| AtRule // @-rule with optional nodes
| Declaration // property: value
| Comment
| Context // metadata wrapper
| AtRoot; // forces children to top-levelConstruct nodes with the small helpers (styleRule, atRule, decl, comment, context, atRoot). The walk(ast, fn, ...) function in walk.ts is how you traverse, and it understands Context and AtRoot semantics. Replacing nodes happens through the WalkAction enum (Continue, Skip, Stop, Replace([...]), ReplaceSkip([...]), ReplaceStop([...])).
Error handling
The compiler throws plain Errors at the boundaries of bad input. For example, parseCss throws on malformed @theme, @source, or @utility directives. Adapter code (CLI, Vite, PostCSS, Webpack) wraps the calls in try/catch and reports through their host:
- CLI:
eprintln(err.toString()); process.exit(1)(packages/@tailwindcss-cli/src/commands/build/index.ts). - PostCSS: clears the cache entry on error (
getCacheKey→cache.delete(key)) so the next attempt retries from scratch. - Vite: lets the error bubble; Vite shows it in the browser overlay.
- Webpack: passes the error to its async callback.
Errors from the v3 compat layer often include a hint about migration. Search the codebase for throw new Error( to see message patterns.
Naming
- TypeScript files use kebab-case (
compile.ts,at-import.ts,apply-compat-hooks.ts). - Test files are siblings with
.test.ts(compile.test.ts). - Benchmarks are siblings with
.bench.ts. - Snapshot directories live next to the test file (
__snapshots__/). - Rust files use snake_case (
candidate_machine.rs,auto_source_detection.rs). - Type names use PascalCase. Functions and variables use camelCase. Enums (
Features,Polyfills,ThemeOptions,WalkAction,CompileAstFlags) are PascalCase with PascalCase members.
"Public" vs "private"
Inside the compiler package, anything exported from a module is fair game for sibling modules. The package's external API surface is what package.json's exports field exposes:
{
".": "./src/index.ts",
"./colors": "./src/compat/colors.ts",
"./defaultTheme": "./src/compat/default-theme.ts",
"./plugin": "./src/plugin.ts",
"./preflight.css": "./preflight.css",
// ... and a few more
}Anything not listed there is internal. Adapter packages reuse internal modules by import { … } from '../../tailwindcss/src/…' (e.g. @tailwindcss/postcss imports toCss from tailwindcss/src/ast). This is intentional — the workspace can cross package boundaries because it's a monorepo.
TypeScript strictness
tsconfig.base.json enables strict mode for every package. There is no JS emission target — tsc is used purely as a type checker via the per-package lint script:
{
"scripts": { "lint": "tsc --noEmit" },
}Bundling is handled by tsup-node (see Tooling).
Dependencies
The compiler package (packages/tailwindcss) has zero runtime dependencies. Only devDependencies. This is intentional — the browser bundle in @tailwindcss/browser includes the compiler at runtime, and it must not pull in magic-string or lightningcss there.
The Node-only optimizations (magic-string, lightningcss, source-map-js, enhanced-resolve, jiti) all live in @tailwindcss/node. Adapters depend on @tailwindcss/node, never directly on tailwindcss's devDependencies.
CSS conventions
- The framework ships four CSS files at the package root:
index.css,preflight.css,theme.css,utilities.css. They are exported viapackage.jsonexports. index.cssis the canonical entry point — it@imports the others.theme.csscontains all default theme tokens (~700 lines). Edits go here, not in TypeScript.preflight.cssis the global reset. It mirrors v3's preflight closely.
Cross-cutting helpers
A handful of small utilities are reused everywhere:
segment(str, sep)(packages/tailwindcss/src/utils/segment.ts) — splits a string on a separator while respecting brackets, parens, and quotes. The grammar is "split unless we're inside a balanced bracket pair."escape(str)/unescape(str)(packages/tailwindcss/src/utils/escape.ts) — CSS identifier escaping.compare(a, b)(packages/tailwindcss/src/utils/compare.ts) — natural sort that handles numeric parts.expand(pattern)(packages/tailwindcss/src/utils/brace-expansion.ts) —a-{1,2,3}→[a-1, a-2, a-3].DefaultMap(packages/tailwindcss/src/utils/default-map.ts) — aMapthat materializes a default on read.
When in doubt, search packages/tailwindcss/src/utils/ before writing your own.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.