Tailwind
Tailwind v4's @theme block is for build-time tokens. djust's tokens
live at runtime and change per request. This page shows the brand-alias
pattern — how to declare Tailwind tokens that resolve through djust's
runtime variables, so bg-brand-rust is generated at build time but its
value still flows from whichever pack is active when the page loads.
The pattern
/* static/css/input.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme {
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
/* Brand aliases — Tailwind generates utilities at compile time;
* the value resolves through djust's runtime CSS variable, so
* swapping the active pack repaints the site without rebuilding
* Tailwind. */
--color-brand-dark: hsl(var(--background));
--color-brand-panel: hsl(var(--card));
--color-brand-rust: hsl(var(--primary));
--color-brand-text: hsl(var(--foreground));
--color-brand-muted: hsl(var(--muted-foreground));
--color-brand-border: hsl(var(--border));
}
@source "../../templates";
@source "../../docs_app";
After make tailwind-build, you can use:
<button class="bg-brand-rust text-brand-text hover:bg-brand-rust/80">
Read the docs
</button>
The class names are static (Tailwind sees them at build time and emits
the rules). The colors are dynamic (the rules reference the runtime
--background etc., which swap per request).
Why two layers?
Tailwind v4 generates utilities from @theme declarations. If you fed
the runtime var(--background) directly into @theme, Tailwind would
freeze its value at build time and you'd lose the runtime swap.
The brand aliases are an indirection — they live at build time (so Tailwind can generate utilities) but they contain runtime references (so the actual paint color is dynamic).
Naming conventions
Two reasonable strategies:
1. Mirror djust's vocabulary
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-card: hsl(var(--card));
Pro: zero translation cost.
Con: Tailwind already has text-primary semantics; collisions are easy.
2. Use a brand prefix
--color-brand-dark: hsl(var(--background));
--color-brand-rust: hsl(var(--primary));
--color-brand-text: hsl(var(--foreground));
--color-brand-muted: hsl(var(--muted-foreground));
--color-brand-border: hsl(var(--border));
Pro: no Tailwind collisions, easy to grep, semantic at a glance. Con: an extra layer of name to maintain.
This is the docs.djust.org convention and what most shipped sites use.
Opacity composition
Tailwind's /N opacity modifier works through the alias because the
runtime variable is the HSL triplet:
<div class="bg-brand-rust/20"></div> <!-- 20% opacity rust -->
<div class="border-brand-border/40"></div>
Internally Tailwind generates background: hsl(var(--primary) / 0.2),
which composes with djust's HSL-triplet convention.
What about @apply?
Avoid @apply for brand utilities — they bake in the build-time alias,
which still respects runtime swaps but adds an indirection that makes
later debugging harder. Inline the utility classes on the elements
instead, or write plain CSS that uses var(--...) directly.
What about other CSS frameworks?
Bootstrap users follow the same pattern via Sass variables. djust ships
a bootstrap-bridge.css that maps shadcn tokens to Bootstrap variables;
see CSS Frameworks.
See also
- Tokens — what each
--background,--primary,--foregroundactually paints. - CSS Frameworks — the broader
framework-abstraction story (Bootstrap 4 / 5, Tailwind, plain) plus
the Bootstrap bridge that does for
.btn-primarywhat brand aliases do for Tailwind. - CSS Frameworks (setup guide) — full Tailwind / Bootstrap installation guide with djust.
- Recipes — patterns for overriding tokens per-page or per-section without a new pack.