djust docs
Browse documentation

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
}
ValueDescription
'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:

  1. Creates static/css/input.css with @import "tailwindcss" and @source directives for your template directories
  2. Creates tailwind.config.js (Tailwind v3) if it doesn't exist
  3. Detects your TEMPLATES directories automatically
  4. 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

VersionConfig fileInput CSS
v4 (recommended)No config needed@import "tailwindcss"; @source "../templates/";
v3tailwind.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 typeBS4 classBS5 class
<select>custom-selectform-select
Checkbox/radio wrappercustom-control custom-checkboxform-check
Form group wrapperform-groupmb-3
Help textform-text text-mutedform-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):

KeyBootstrap 4 defaultBootstrap 5 default
radio_classcustom-control-inputform-check-input
radio_label_classcustom-control-labelform-check-label
radio_wrapper_classcustom-control custom-radioform-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:

ClassEquivalent
.flex-betweendisplay: 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.