Tailwind & styles
Themes use Tailwind v4 standalone (no PostCSS, no Node toolchain). The
bin/tailwindcss binary lives in cms-public/bin/ and is the same
binary used at theme deploy time on the cms-public-worker pod.
styles.src.css — the Tailwind source
This is the only CSS file a theme directly authors. Compiled output
(styles.css) is produced at deploy time and lives next to the source
in the deployed version dir on NFS.
A typical opening:
@import "tailwindcss";
/* Tell Tailwind which template files to scan for class names. Adjust
the depth so it matches this theme's directory layout. The path is
resolved at compile time, relative to this file. */
@source "../templates/**/*.twig";
/* Custom theme tokens — these become CSS variables AND Tailwind
utilities (bg-cream, text-gold-deep, etc.). */
@theme {
--color-bg: #f8f4ee;
--color-cream: #f1ead9;
--color-paper: #ffffff;
--color-ink: #2a2117;
--color-ink-soft: #5e5440;
--color-ink-deep: #1a1611;
--color-gold: #d2b07a;
--color-gold-deep: #b89968;
--color-divider: #e1d6c2;
--font-display: "Playfair Display", serif;
--font-script: "Allura", cursive;
--font-sans: "Inter", system-ui, sans-serif;
}
/* Theme-private utilities and base styles. Anything that's not
reachable through Tailwind classes lives here. */
@layer base {
body { color: var(--color-ink); background: var(--color-bg); }
}
Arbitrary CSS is fine after the @import "tailwindcss" line: Tailwind
v4 just preserves it. Keep the theme's own base styles and helpers inside
@layer base / @layer utilities so their order against Tailwind's
utilities is predictable. Overrides of the CSS cms-public ships are the
exception: those go outside any @layer, see
Overriding cms-public's CSS.
How @source works
@source "<glob>" tells the Tailwind v4 engine where to look for class
names. The path is relative to styles.src.css, resolved at compile
time. For a theme repo laid out like this:
my-theme/
├── styles.src.css
└── templates/
├── base.html.twig
└── pages/
└── home.html.twig
…use @source "../templates/**/*.twig"; (the source file is at the repo
root, templates one level down). Get this path wrong and Tailwind ships
no utility classes — pages render unstyled.
What the deploy compile does
When the worker pod runs ThemeCompiler::compileVersion():
- Locates the deployed version dir on NFS:
/data/cms/themes/<themeCode>/<version>/. - Runs
bin/tailwindcss -i styles.src.css -o styles.css --minify. - Writes
styles.cssnext to the source — that's whattheme_asset('styles.css')resolves to.
There's a 60s timebox on the compile to catch runaway @source globs.
If the binary returns non-zero, the deploy is recorded as failed in the
theme_deployment_status table and the active version doesn't change.
Local Tailwind workflow
Two ways:
-
Watch mode against your theme repo while editing templates:
cd ~/path-to-my-theme ~/path-to-cms-public/bin/tailwindcss -i styles.src.css -o styles.css --watch -
Render against the local pod's NFS mount — same binary, same inputs, same output as production.
kubectl --context local exec -n cms cms-public-worker-0 -- \ php bin/console app:theme:compile <themeCode> <version>(Command name + flags may differ — check
php bin/console list appinside the pod.)
Shared block defaults
cms-public/public/assets/blocks.css ships with the site (loaded by
every page from _shared/base.html.twig). It defines the four
image-field knobs ([data-width], [data-align], [data-radius],
[data-fit]) so every theme inherits sensible defaults without
re-implementing them. Themes can override any of these in their own CSS,
outside any @layer. Load order: blocks.css, then the theme's
styles.css, then the per-page module CSS below.
Per-page module CSS
The cms_module() system links its CSS with <link rel="stylesheet">
tags that _shared/base.html.twig puts at the end of <head>, after the
theme's own stylesheet; see per-page-modules.md.
Module CSS uses --cms-<name>-* custom properties as tuning knobs, and
the stylesheets that have them declare the defaults on the module's own
root element:
/* slideshow.css */
.cms-slideshow {
--cms-slideshow-cta-bg: #111;
--cms-slideshow-heading-weight: 500;
/* … */
}
That decides where a theme can set them. A value on an ancestor (a
wrapping <section style="…">) never reaches the module, because the
root element's own declaration beats an inherited value. A theme rule on
.cms-slideshow alone loses too: same specificity, and the module's
sheet loads later. Set them on the root element with a more specific
selector, outside any @layer:
/* end of styles.src.css, outside any @layer */
body .cms-slideshow {
--cms-slideshow-cta-bg: var(--color-gold-deep);
--cms-slideshow-heading-weight: 700;
}
For different tuning per page, key the selector on a class you put on a wrapper:
<section class="hero-dark">
{{ field('hero', 'slideshow') }}
</section>
.hero-dark .cms-slideshow { --cms-slideshow-overlay-bg: rgba(0, 0, 0, 0.5); }
Overriding cms-public's CSS
blocks.css and every per-page module stylesheet (slideshow.css,
reviews.css, faq.css, …) are unlayered: none of them uses
@layer. In the cascade an unlayered rule beats a rule in any layer,
whatever the specificity and whatever the load order. So this never
applies:
@layer components {
.page .cms-reviews .cms-reviews__text { font-size: 1rem; } /* loses to reviews.css */
}
The same goes for a Tailwind utility on module markup. Tailwind v4 emits
its utilities inside @layer utilities, so where the module's CSS sets
the same property, the module wins.
Write overrides of module classes, and of their custom properties,
outside any @layer, at the end of styles.src.css. Out there they
compete on ordinary specificity and source order:
| Stylesheet | Loads | A theme rule wins with |
|---|---|---|
blocks.css |
before the theme's stylesheet | equal or higher specificity |
| per-page module CSS | after the theme's stylesheet, end of <head> |
higher specificity |
/* end of styles.src.css, outside any @layer */
.cms-reviews .cms-reviews__text { font-size: 1rem; } /* beats reviews.css's .cms-reviews__text */
What you don't ship
node_modules/— there's no Node toolchain.package.json— same.- A bundler config (Vite, esbuild, webpack) — same.
- Pre-compiled
styles.cssin your repo — the deploy compiler produces it. Don't commit the output. - Source maps — Tailwind v4 standalone doesn't emit them.