CSS Frameworks
Set up Tailwind CSS or Bootstrap 5 with djust, configure the css_framework setting, and integrate CSS builds into CI/CD.
On this page
djust ships with built-in support for Tailwind CSS and Bootstrap 5. The framework you choose affects how djust renders form fields and components automatically — no manual widget styling required.
Quick Setup
One command sets up and builds your CSS:
# Tailwind CSS (recommended)
python manage.py djust_setup_css tailwind
# Bootstrap (manual install — see below)
python manage.py djust_setup_css bootstrap
# No CSS framework (plain HTML)
python manage.py djust_setup_css none
Configuring the CSS Framework
Add css_framework to LIVEVIEW_CONFIG in settings.py:
LIVEVIEW_CONFIG = {
'css_framework': 'tailwind', # 'tailwind', 'bootstrap5', or None
}
| Value | Description |
|---|---|
'bootstrap5' | Bootstrap 5 classes on forms and components (default) |
'bootstrap4' | Bootstrap 4 classes — for legacy projects, NYC Core Framework, government sites |
'tailwind' | Tailwind utility classes on forms and components |
None / 'plain' | Unstyled plain HTML |
This setting controls how FormMixin and djust's built-in components render their fields. You can still use any CSS framework for your own templates — this only affects the auto-generated widget HTML.
Tailwind CSS
One-command Setup
python manage.py djust_setup_css tailwind
This command:
- Creates
static/css/input.csswith@import "tailwindcss"and@sourcedirectives for your template directories - Creates
tailwind.config.js(Tailwind v3) if it doesn't exist - Detects your
TEMPLATESdirectories automatically - Runs the Tailwind CLI to compile
static/css/output.css
Prerequisites
The command auto-detects the Tailwind CLI. Install it via npm:
npm install -D tailwindcss
Or use the standalone CLI (no Node required):
# macOS ARM
curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-arm64
chmod +x tailwindcss-macos-arm64
settings.py Requirements
STATICFILES_DIRS must be configured so the command knows where to write the output CSS:
BASE_DIR = Path(__file__).resolve().parent.parent
STATICFILES_DIRS = [BASE_DIR / "static"]
Add CSS to Base Template
After running the setup command, link the compiled CSS in your base template:
{% load static %}
<link rel="stylesheet" href="{% static 'css/output.css' %}">
Watch Mode (Development)
Auto-rebuild when templates change:
python manage.py djust_setup_css tailwind --watch
Tailwind v3 vs v4
| Version | Config file | Input CSS |
|---|---|---|
| v4 (recommended) | No config needed | @import "tailwindcss"; @source "../templates/"; |
| v3 | tailwind.config.js with content: paths | @tailwind base; @tailwind components; @tailwind utilities; |
djust_setup_css always writes a v4-style input.css, and it also writes a v3-style tailwind.config.js if none exists. It does not detect your Tailwind version. On Tailwind v3, replace the contents of input.css with the three @tailwind directives.
Bootstrap 5
Manual Setup
Bootstrap does not require a build step if you use the CDN:
<!-- In your base template <head> -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
Configure djust to use Bootstrap 5 styling:
LIVEVIEW_CONFIG = {
'css_framework': 'bootstrap5',
}
npm Install
For a self-hosted setup:
npm install bootstrap
Then import in your CSS entry point:
@import "bootstrap/dist/css/bootstrap.min.css";
Bootstrap + Sass Compilation
For custom theming with Sass variables:
npm install bootstrap sass
// static/scss/main.scss
$primary: #6366f1; // Override Bootstrap variables
$border-radius: 0.5rem;
@import "bootstrap/scss/bootstrap";
Compile:
npx sass static/scss/main.scss static/css/main.css
Bootstrap 4
Bootstrap 4 still ships across many legacy projects, government sites,
and frameworks like NYC Core. djust's Bootstrap4Adapter (added in
v0.5.0) renders form widgets with the BS4 class set instead of BS5.
# settings.py
LIVEVIEW_CONFIG = {
'css_framework': 'bootstrap4',
}
The adapter handles the four BS4-specific class differences from BS5:
| Element type | BS4 class | BS5 class |
|---|---|---|
<select> | custom-select | form-select |
| Checkbox/radio wrapper | custom-control custom-checkbox | form-check |
| Form group wrapper | form-group | mb-3 |
| Help text | form-text text-muted | form-text |
Install Bootstrap 4 the same way you would for any Bootstrap project — bring your own Bootstrap 4 CSS via npm/CDN. djust does not bundle it.
Radio buttons (separate config keys)
Radio inputs use their own config keys rather than reusing the
checkbox classes (BS4 and BS5 style them differently). Override them in a
custom adapter, or in LIVEVIEW_CONFIG under the framework key (see below):
| Key | Bootstrap 4 default | Bootstrap 5 default |
|---|---|---|
radio_class | custom-control-input | form-check-input |
radio_label_class | custom-control-label | form-check-label |
radio_wrapper_class | custom-control custom-radio | form-check |
If a radio key is not set, djust falls back to the corresponding checkbox key — so existing Bootstrap 5 projects don't need to define radio classes explicitly.
Top-level keys such as LIVEVIEW_CONFIG = {'radio_class': ...} are ignored:
classes are looked up as <framework>.<key>. LIVEVIEW_CONFIG is also merged
shallowly, so setting 'bootstrap4': {...} replaces the whole Bootstrap 4
class map. Copy the full default map when you override one key:
LIVEVIEW_CONFIG = {
"css_framework": "bootstrap4",
"bootstrap4": {
# The full default map, with radio_class changed:
"field_class": "form-control",
"field_class_invalid": "form-control is-invalid",
"select_class": "custom-select",
"error_class": "invalid-feedback",
"error_class_block": "invalid-feedback d-block",
"help_text_class": "form-text text-muted",
"label_class": "",
"checkbox_class": "custom-control-input",
"checkbox_label_class": "custom-control-label",
"checkbox_wrapper_class": "custom-control custom-checkbox",
"radio_class": "my-radio",
"radio_label_class": "custom-control-label",
"radio_wrapper_class": "custom-control custom-radio",
"field_wrapper_class": "form-group",
"button_primary_class": "btn btn-primary",
"button_secondary_class": "btn btn-secondary",
},
}
Select widgets
ChoiceField rendered with widget=Select reads its class from
select_class (default form-select for BS5, custom-select for
BS4) instead of the generic field_class used for <input> fields.
This keeps native dropdowns from picking up the wrong borders/padding.
Theme tokens → framework classes
djust's theming system exposes design tokens as CSS variables
(--primary, --border, --background, …). The
{% theme_framework_overrides %} template tag emits a <style> block
that maps those tokens onto the active framework's class selectors so
a theme switch automatically restyles Bootstrap components without
editing Bootstrap source:
{% load theme_tags %}
<head>
…
{% theme_framework_overrides %}
</head>
Example output (Bootstrap 5, abridged):
.form-control { color: hsl(var(--foreground)); background-color: hsl(var(--input, var(--card))); border-color: hsl(var(--border)); }
.btn-primary { --bs-btn-bg: hsl(var(--primary)); --bs-btn-border-color: hsl(var(--primary)); --bs-btn-color: hsl(var(--primary-foreground)); }
.alert-info { background-color: hsl(var(--info) / 0.1); border-color: hsl(var(--info) / 0.3); color: hsl(var(--info)); }
The tokens are HSL triplets, so every rule wraps them in hsl(...).
The tag emits nothing unless a theme pack is active. It is also a no-op
when css_framework='tailwind' or None — Tailwind projects manage their
palette via @theme in input.css, so the bridge would just duplicate work.
How djust Uses the CSS Framework
When css_framework is set, djust's FormMixin and built-in components automatically apply framework classes to rendered widgets.
Form Field Rendering
# In your LiveView
class ContactView(FormMixin, LiveView):
form_class = ContactForm
With css_framework='bootstrap5', a required EmailField renders as:
<div class="mb-3">
<label for="id_email" class="form-label">Email <span class="text-danger">*</span></label>
<input name="email" id="id_email" class="form-control" required="required"
dj-change="validate_field" type="email" value="" maxlength="320" />
</div>
With css_framework='tailwind', fields render as:
<div class="mb-4">
<label for="id_email" class="block text-sm font-medium text-gray-700">Email <span class="text-red-600">*</span></label>
<input name="email" id="id_email"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
required="required" dj-change="validate_field" type="email" value="" maxlength="320" />
</div>
With css_framework=None, fields render as unstyled plain HTML.
Custom Adapter
Register a custom adapter to override framework classes:
from djust.frameworks import BaseAdapter, register_adapter
class MyAdapter(BaseAdapter):
# Static HTML only — never interpolate user data here
required_marker = ' <span class="required">*</span>'
help_text_class = "hint"
register_adapter('my_theme', MyAdapter())
# settings.py
LIVEVIEW_CONFIG = {'css_framework': 'my_theme'}
Production Builds
Minified CSS
# Tailwind
python manage.py djust_setup_css tailwind --minify
# Sass/Bootstrap
npx sass static/scss/main.scss static/css/main.min.css --style compressed
CI/CD Integration
Add the CSS build step before collectstatic in your CI pipeline:
# GitHub Actions example
- name: Install Node dependencies
run: npm ci
- name: Build CSS
run: python manage.py djust_setup_css tailwind --minify
- name: Collect static files
run: python manage.py collectstatic --noinput
Docker
# In your Dockerfile — before collectstatic
RUN npm ci
RUN python manage.py djust_setup_css tailwind --minify
RUN python manage.py collectstatic --noinput
Makefile Targets
css:
python manage.py djust_setup_css tailwind
css-watch:
python manage.py djust_setup_css tailwind --watch
css-prod:
python manage.py djust_setup_css tailwind --minify
Demo utility classes
The djust demo project ships a tiny utilities.css you can crib
from. The most-reached-for class:
| Class | Equivalent |
|---|---|
.flex-between | display: flex; align-items: center; justify-content: space-between; |
Use it on card headers or any flex container that needs a title on the left and an action widget on the right — saves repeating the same three flex declarations across templates. It's not a djust runtime requirement, just a convention worth borrowing.
Common Pitfalls
Four ways CSS goes wrong in production. Three of them have a system check that
catches them, so python manage.py check finds them before a deploy does.
Tailwind CDN in production
<script src="https://cdn.tailwindcss.com"></script> is a development
convenience only: roughly 300 KB uncompressed, render-blocking, with no
tree-shaking. Compile instead. Caught by djust.C010.
Loading client.js by hand
Adding <script src="{% static 'djust/client.js' %}"> duplicates what djust
auto-injects for every LiveView page — double initialisation and a race
between the two. Remove the tag. Caught by djust.C012.
Deploying without compiling CSS
The development fallback does not exist in production, so the page ships
unstyled. Run the build before deploying. Caught by djust.C011, which
warns when output.css is missing.
Template paths that don't match
If tailwind.config.js content globs don't match the real template
directories, the build purges classes that are actually used — the utilities
go missing and the result can be larger, not smaller. No system check
covers this one. Use
djust_setup_css to auto-detect the directories, or check the globs by hand.
CI/CD Integration
GitHub Actions
name: Build CSS
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: |
pip install -r requirements.txt
npm install -D tailwindcss
- name: Build CSS
run: python manage.py djust_setup_css tailwind --minify
- name: System checks
run: python manage.py check
Docker
FROM python:3.12-slim
RUN apt-get update && apt-get install -y nodejs npm
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
RUN npm install -D tailwindcss
RUN python manage.py djust_setup_css tailwind --minify
RUN python manage.py check
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
Custom Adapters
There is no djust.css.CSSAdapter module and no DJUST_CSS_ADAPTER
setting. Two real customization points exist:
Generated files. djust_setup_css tailwind writes tailwind.config.js
and the input CSS only when they are absent, so pre-create or edit those files
to change content globs, theme, or plugins — the build respects them and
prints the command it ran.
Form-field classes. The simplest route is to subclass
djust.frameworks.BaseAdapter and override class attributes, as shown in
Custom Adapter above. To take over rendering completely,
subclass djust.frameworks.FrameworkAdapter and implement its three abstract
methods with these signatures:
from djust.frameworks import FrameworkAdapter, register_adapter
class MyAdapter(FrameworkAdapter):
def render_field(self, field, field_name, value, errors, **kwargs): ...
def render_errors(self, errors, **kwargs): ...
def get_field_class(self, field, has_errors=False): ...
register_adapter("myui", MyAdapter())
Troubleshooting
STATICFILES_DIRS not configured error
Add to settings.py:
STATICFILES_DIRS = [BASE_DIR / "static"]
Tailwind CLI not found
Install via npm (npm install -D tailwindcss) or download the standalone binary. See Tailwind installation.
Classes not being applied
Check that your input.css has @source directives (v4) or content: paths (v3) pointing to your template directories. Run djust_setup_css tailwind again to regenerate.
Bootstrap JS not loading
Bootstrap's JavaScript (dropdowns, modals) must be loaded separately. Add the bootstrap.bundle.min.js script tag or import it via npm.