Tailwind Build

Tailwind CSS v4 is wired into every Leed site. There is nothing to install, no tailwind.config.js anywhere in the product, and no <link> tag for you to write. Configuration in v4 is CSS, and your entry point is one file in your repository.

Detection: one file turns CSS on

The build looks for exactly one path:

<your-repo>/tailwind/site.config.css

If it exists, leed.env.css.usingTailwind is set and the Tailwind plugin is registered. If it does not, the build logs

Tailwind is not enabled, skipping compilation

at debug level — so on a normal build you will not see it at all — and the site ships with no stylesheet. That same flag is what makes leed/head emit the stylesheet tags, so a missing entry file produces pages with no CSS and no error.

The one line your entry file must contain

Everything Leed adds to your CSS is inserted immediately after the first line matching @import "tailwindcss". The scaffold a new site starts from puts it on line one:

@import "tailwindcss" source("../src");

/**
 * Layouts
 */
@import "./layouts/blog.css" layer(components);

@import "./site/base.css" layer(base);

A file with no such line still compiles — Tailwind will process it — but it compiles with zero Leed CSS, no scanning of Leed’s own shared templates, and no exclusion of your archive and trash directories. Nothing warns you.

What gets injected

Four lines, inserted verbatim in this order:

@source not "../src/_archive";
@source not "../src/_trash";
@source "<leed-app-root>/static/site/**/*.hbs";
@import "<relative-path-to>/leed.css";
LineKindWhat it doesWhat breaks without it
@source not "../src/_archive"scan exclusionKeeps archived pages out of class scanningArchived content resurrects classes into the build
@source not "../src/_trash"scan exclusionSame, for trashed pagesDeleted content keeps its CSS alive
@source "<appRoot>/static/site/**/*.hbs"scan inclusionScans Leed’s own shared partials, so the classes they use survive tree-shakingLeed’s header, sidebar, pager, search and alert markup renders unstyled
@import "<…>/leed.css"stylesheetPulls in Leed’s entire stylesheet — layer order, defaults, components, docs CSS, themes, fonts, Font AwesomeYou get Tailwind and nothing else

The @import position matters more than it looks. Leed’s stylesheet is imported at the top of your file, before your own imports — which is precisely what lets a layer(components) file of yours win over a Leed component style on ordinary cascade rules, with no !important. That mechanism is the subject of Cascade Layers and Overriding Leed.

source("../src")

The scaffold’s own directive. It points Tailwind’s class scanner at your src/ tree — your templates, your partials and your content. The three injected @source lines add to it rather than replacing it.

What “scanned source” includes, and why it matters more than it looks

Tailwind v4 emits a utility only when it finds the class name somewhere in scanned source, as text. The scan root is your content repository’s src/, and JSON data files under it are scanned like any other file.

That is not a curiosity. Measured on a shipped Pithy build, the emitted stylesheet contains fa-swift, fa-android, fa-unity, fa-brackets-curly, fa-i-cursor and fa-laptop-code — and those class names appear nowhere in the site except src/docs/docs.11tydata.json, where they are tab-group icons.

Two consequences follow, and they point in opposite directions:

  • A Font Awesome icon named only in a tab-group configuration does get its CSS, because the configuration file is scanned.
  • A tab group configured only in the CMS company record, and therefore never published into a file under src/, renders an icon class with no rule behind it. Same mechanism, opposite outcome.

The same reasoning covers theme names. documentationConfiguration.colorTheme stores the full class string, and it lands in src/docs/docs.11tydata.json on publish — which is what makes a custom @utility color-theme-<brand> emit at all. If your documentation ever renders in Leed’s default blue after a page-type publish, check that the class literal still survives that round trip.

What a tailwind/ directory looks like

The scaffold above is the whole starting point. As a site grows, the shape that scales is one entry file plus directories by role — and every import carrying an explicit layer(...) annotation (or a comment explaining why it deliberately has none).

The full tailwind/ tree from leed.ai's own site
tailwind/
├── site.config.css
├── docs/
│   ├── chrome.css
│   ├── code-theme-leed.css
│   ├── color-theme-leed.css
│   ├── components.css
│   └── mermaid.css
├── layouts/
│   ├── apply.css
│   ├── blog.css
│   ├── centered.css
│   ├── content-with-form.css
│   ├── home.css
│   ├── legal.css
│   ├── pricing.css
│   ├── products.css
│   └── solution-page.css
├── partials/
│   ├── alerts.css
│   ├── cards.css
│   ├── comparisons.css
│   ├── footer.css
│   ├── forms.css
│   ├── illustrations.css
│   ├── menu.css
│   ├── pagination.css
│   ├── platform.css
│   ├── recommendations.css
│   └── sections.css
└── site/
    ├── base.css
    ├── buttons.css
    ├── components.css
    ├── images.css
    ├── svg-diagrams.css
    ├── theme.css
    └── utilities.css

site/theme.css is imported first and holds the token palette; docs/ holds everything that only applies inside the documentation shell; layouts/ and partials/ mirror the template tree. The entry file’s comments record, per import, why it is in a layer or out of one — worth reading in full before you write your own.

The PostCSS chain

Your file, with Leed’s four lines inserted, goes through three PostCSS plugins in this fixed order:

#PluginEffectActive when
1@tailwindcss/postcssResolves @import, @theme, @utility, @apply and @source; emits only the utilities found in scanned sourceAlways
2postcss-urlRewrites asset URLs (below)Always
3fileLoggerWrites the compiled CSS out for inspectionDebug builds

Any PostCSS warning is logged at warn level rather than swallowed, and an empty result throws No CSS output from preprocessing step!.

The URL rewrite is a simple, useful rule with one sharp edge: it looks at the pathname, not at where the file is.

  • A url() whose pathname contains webfonts/ becomes /static/webfonts/<everything after webfonts/>, query string preserved.
  • A url() whose pathname contains images/ becomes /static/images/<everything after images/>, query string preserved.
  • Everything else passes through untouched.

So a relative url("../webfonts/MyFace.woff2") written from anywhere under tailwind/ resolves correctly in the output. Where the font files themselves go is covered in Fonts and Webfonts.

Output, hashing and caching

The compile runs once per build, before templates render. The result is written on Eleventy’s after event.

ItemValueSet byConsumed by
Output path<output>/static/css/tailwind.csseleventy.after hook, fs.writeFileSyncThe browser
Content hasha hash of the compiled CSScompileCSS()The ?v= query string
leed.env.css.tailwindCSSPath/static/css/tailwind.css?v=<hash>The Tailwind pluginleed/head
leed.env.css.usingTailwindtrue when the entry file existsaddTailwindPluginleed/head, as the condition on both tags
<link> tagsone rel="preload" as="style", one rel="stylesheet"leed/head.hbsThe browser
Cache headerCache-Control: public, max-age=31536000, immutable on /static/*leed-headers.hbsThe CDN and the browser

Two details are worth calling out because they look like mistakes and are not.

Two link tags, not the combined form. Leed emits rel="preload" as="style" and rel="stylesheet" as separate tags rather than the shorter rel="preload stylesheet". Loading priority is identical; the reason is that the dev server’s CSS hot reload iterates link[rel="stylesheet"] as an exact attribute match, and the combined form matched nothing — so a CSS edit never reached the browser.

The hash is the whole point of the cache header. /static/* is served immutable for a year, so a stylesheet whose URL never changed would never be re-fetched. The ?v=<hash> changes on every content change and nothing else. That header is suppressed on preview deployments. The full caching story is in Static Files and Caching.

Is it minified?

No. The Tailwind stylesheet is tree-shaken but not minified — in every build mode.

This surprises people, so here is the mechanism. The stylesheet is written with fs.writeFileSync from an eleventy.after hook, straight from the PostCSS output. Eleventy transforms — including Leed’s minify transform — only ever see template output, so the stylesheet bypasses the transform chain entirely. There is no cssnano step inside the Tailwind PostCSS chain either.

Verified against a real build: 614 KB, 19,409 lines, comments intact. HTML, XML, JSON and JavaScript emitted through Eleventy are minified in non-debug builds; CSS is not.

Tree-shaking still does the heavy lifting. Only utilities found in scanned source are emitted at all — one of fourteen syntax themes, one of seventeen documentation color themes on a typical site.

The watch loop

The plugin registers addWatchTarget("<repo>/tailwind/**/*", { resetConfig: true }). Under leed site build --serve, any file under tailwind/ triggers a full configuration reset, rebuild and CSS recompile. Editing a page or a template does not recompile CSS — the stylesheet from the previous compile is simply rewritten unchanged.

Running the compile continuously is the normal working loop, and it is described alongside the rest of the dev server in Local Development.

When the compile fails

A PostCSS or Tailwind error is logged as

Tailwind Processing Error! <message>

and then the process behavior splits on mode:

  • A normal build calls process.exit(1). The build fails, and nothing is deployed with a broken stylesheet.
  • Under --serve the server stays up. The error is logged, the previous CSS remains in place, and you fix the file and save. That asymmetry is deliberate: a dev server that died on a mistyped selector would be unusable.

The same split applies to a failure writing the output file.

tailwind/ is fully editable, but a few sibling paths are not — src/static/images/logo-* and favicon-* among them. The complete list is in Editable and Read-Only Files.

ESC