CSS System: Variables, Selectors and No Tailwind
Plain CSS files, generated colour and size variables, light and dark themes by attribute selector, and a build that bundles, validates and minifies.
AstroWEBTheme does not use Tailwind. Styling is hand-written CSS plus a small set of generated variables and utility classes. Both editions work this way, which keeps the markup free of long class lists and the build free of a utility compiler.
Tokens come from config
Three files in src/site/config/styles/ generate the variables you use everywhere:
| Config | Generator | Output |
|---|---|---|
colors.mjs |
colors-css.mjs |
src/site/styles/main/colors.css: light and dark colour variables, plus bg-, col-, br- and filter- utilities |
variables.mjs |
variables-css.mjs |
src/site/styles/main/variables.css: layout widths, radius, borders, shadows, transitions |
fonts.mjs |
fonts-css.mjs |
Font faces and the size, weight and line-height tokens (see Self-hosted fonts) |
To rebrand, change brand, accent and neutral in colors.mjs. Contrast pairs are chosen to meet WCAG AA.
Selectors
- Theme by attribute. Colours are declared under
:root, [data-theme="light"]and[data-theme="dark"]. The header toggle only changesdata-themeon<html>, so every component follows without extra CSS. - Variables in components. Rules read tokens such as
var(--on-brand-base)rather than raw colours. - Utilities. Small single-purpose classes live in
core/styles/main/utilities-*.css(display, flex and grid, space, size, position, colours). Responsive variants carry a prefix such aslg-orfull-. - Component state by data attribute. The callout uses
.callout[data-variant="ctaPrimary"], so variants are styled by selector, not by new classes.
Branches and order
src/site/config/styles/styles.mjs is the single source for what goes where.
| Branch | Sources | Output |
|---|---|---|
inline |
core/styles/inline, src/site/styles/inline |
Generated TypeScript module, printed in the head |
main |
core/styles/main, src/site/styles/main |
public/styles/main.[hash].css |
components |
core/styles/components, src/site/styles/components |
Hashed bundle, loaded lazily |
non-critical |
core/styles/non-critical, src/site/styles/non-critical |
Hashed bundle, loaded lazily |
| page CSS | src/site/styles/pages/*.css |
public/styles/pages/<name>.[hash].css, requested per page with FilesToLoadCSS |
Folders define membership. The order array only pins important files first. For a file with the same name, the core rules come first and the site rules follow, so your CSS wins through the normal cascade without !important.
Where PostCSS fits
postcss and autoprefixer are declared as dev dependencies, but the bundlers in core/scripts/build/css-bundles.mjs and css-page.mjs do not import them. Astro is configured with css.transformer: "lightningcss", and the bundlers use lightningcss to parse every source file (a syntax error stops the build with a line and column), then minify the combined bundle. If stylelint is installed, the minified output is linted too. If your project needs a PostCSS plugin, add it as a project step; the theme does not require one.
Commands
pnpm run generate-css # run the whole pipeline
pnpm run generate-css:dry # show inputs, outputs and stale steps; writes nothing
pnpm run generate-css:force # rebuild every output
The order is: colours, fonts, variables, main, inline, components, page CSS, non-critical. Manifest data goes to src/data/manifest/styles.json. Details live in core/scripts/generate/css.md.
Where to put your CSS
- Project styling:
src/site/styles/main/orsrc/site/styles/pages/. - Do not edit
core/styles/for one client; keep core reusable. - Never hand-edit
colors.css,variables.cssorfonts.css; they are regenerated.