CSS Frameworks
A theme pack paints the surface — colors, type, motion. A CSS
framework decides what class="…" your form fields and built-in
components emit (form-control vs form-select vs a Tailwind utility
chain). djust separates the two concerns: pick a theme pack for the
look, pick a CSS framework for the markup, and the same Python code
renders correctly under either.
This page covers the framework abstraction: the four shipped adapters, the class-name registry, how theme tokens flow into Bootstrap classes via the bridge, and how to add a fifth (Bulma, UIKit, your own).
The shipped adapters
Set the active adapter in LIVEVIEW_CONFIG:
LIVEVIEW_CONFIG = {
'css_framework': 'tailwind', # 'bootstrap5' | 'bootstrap4' | 'tailwind' | None
}
| Value | What it emits | Use when |
|---|---|---|
'bootstrap5' | form-control, form-select, mb-3, btn btn-primary | Default. Modern Bootstrap. |
'bootstrap4' | form-control, custom-select, form-group, btn btn-primary | Legacy projects, government / NYC Core Framework. |
'tailwind' | Long utility chains tuned for the v3/v4 default config. | Tailwind projects. |
None / 'plain' | Empty class strings; semantic HTML only. | No CSS framework, or you supply your own. |
FormMixin, the built-in components, and the {% form %} template tag
all delegate to the active adapter — no per-handler wiring.
The two layers
1. FrameworkAdapter — render-time logic
Each framework has rendering quirks (Bootstrap 5 wraps inputs
differently than Bootstrap 4; Tailwind wants utility classes on the
<input> itself, not a wrapper). djust models that with the
FrameworkAdapter ABC:
class FrameworkAdapter(ABC):
@abstractmethod
def render_field(self, field, field_name, value, errors, **kwargs) -> str: ...
@abstractmethod
def render_errors(self, errors, **kwargs) -> str: ...
@abstractmethod
def get_field_class(self, field, has_errors=False) -> str: ...
Concrete subclasses live in djust.frameworks:
Bootstrap4Adapter—form-groupwrapper,custom-selectfor<select>, custom checkbox/radio markup.Bootstrap5Adapter—mb-3wrapper,form-select,form-checkpatterns.TailwindAdapter— utilities directly on the input;mb-4wrapper.PlainAdapter— empty class strings; the markup stays semantic.
from djust.frameworks import get_adapter
adapter = get_adapter() # current active (default: bootstrap5)
adapter = get_adapter('tailwind') # explicit
html = adapter.render_field(my_form['email'], 'email', None, errors=[])
2. The class-name registry — what classes get emitted
For each framework, a flat dict maps abstract class roles to actual CSS class strings. Bootstrap 5's defaults:
'bootstrap5': {
'field_class': 'form-control',
'field_class_invalid': 'form-control is-invalid',
'select_class': 'form-select',
'error_class': 'invalid-feedback',
'help_text_class': 'form-text',
'label_class': 'form-label',
'checkbox_class': 'form-check-input',
'checkbox_label_class': 'form-check-label',
'checkbox_wrapper_class': 'form-check',
'radio_class': 'form-check-input',
'radio_wrapper_class': 'form-check',
'field_wrapper_class': 'mb-3',
'button_primary_class': 'btn btn-primary',
'button_secondary_class': 'btn btn-secondary',
}
Bootstrap 4's are similar with the differences (custom-select,
custom-control-input, form-group); Tailwind's are utility chains;
Plain is empty strings. Adapters resolve via
config.get_framework_class('field_class'), so changing the active
framework swaps the values without code changes.
Overriding individual classes
Two ways. Settings:
LIVEVIEW_CONFIG = {
'css_framework': 'tailwind',
'tailwind': {
'field_class': 'block w-full rounded-lg border-zinc-700 bg-zinc-900 text-zinc-100',
'field_wrapper_class': 'mb-6',
'button_primary_class': 'rounded-md bg-emerald-500 px-4 py-2 font-semibold text-white hover:bg-emerald-600',
},
}
Or programmatically at startup:
from djust.config import get_config
config = get_config()
config.set('tailwind.button_primary_class', 'btn btn--brand')
Override only the slots you care about; the rest fall back to the shipped defaults.
The Bootstrap bridge — theme tokens flow into Bootstrap classes
The theme pack's color tokens (--primary, --card, --border, …)
need to reach Bootstrap classes (.btn-primary, .card, .form-control)
without you editing Bootstrap's source. djust's
pack_css_generator.py emits a small bridge stylesheet that wires
them up:
/* What pack_css_generator emits when css_framework == 'bootstrap5' */
.btn-primary {
--bs-btn-bg: hsl(var(--primary));
--bs-btn-border-color: hsl(var(--primary));
--bs-btn-color: hsl(var(--primary-foreground));
}
.form-control {
color: hsl(var(--foreground));
background-color: hsl(var(--input, var(--card)));
border-color: hsl(var(--border));
}
.form-control:focus {
border-color: hsl(var(--ring, var(--primary)));
box-shadow: 0 0 0 var(--form-focus-ring-width, 0.25rem)
hsl(var(--ring, var(--primary)) / var(--form-focus-ring-opacity, 0.25));
}
.card {
background-color: hsl(var(--card));
color: hsl(var(--card-foreground));
border-color: hsl(var(--border));
}
The result: visitors using bootstrap5 get theme-aware Bootstrap.
Switch the active theme pack and .btn-primary recolors immediately
without rebuilding any CSS.
For Tailwind there's no bridge needed because you already declare
brand aliases inside @theme (see Tailwind) that
resolve through the same runtime tokens.
Authoring a custom adapter
For frameworks djust doesn't ship — Bulma, UIKit, Pico, Open Props, an
in-house design system — subclass BaseAdapter, add the framework's
class-name dict to LIVEVIEW_CONFIG, register the adapter at startup:
# my_app/css_adapters.py
from djust.frameworks import BaseAdapter
class BulmaAdapter(BaseAdapter):
"""Render fields with Bulma classes."""
required_marker = ' <span class="has-text-danger">*</span>'
help_text_tag = 'p'
help_text_class = 'help'
error_wrapper = True
# settings.py
LIVEVIEW_CONFIG = {
'css_framework': 'bulma',
'bulma': {
'field_class': 'input',
'field_class_invalid': 'input is-danger',
'select_class': 'select',
'error_class': 'help is-danger',
'help_text_class': 'help',
'label_class': 'label',
'checkbox_class': 'checkbox',
'checkbox_wrapper_class': 'control',
'field_wrapper_class': 'field',
'button_primary_class': 'button is-primary',
'button_secondary_class': 'button is-light',
},
}
# my_app/apps.py
class MyAppConfig(AppConfig):
name = 'my_app'
def ready(self):
from djust.frameworks import register_adapter
from .css_adapters import BulmaAdapter
register_adapter('bulma', BulmaAdapter())
That's it — FormMixin, the built-in components, and {% form %}
all start emitting Bulma classes.
The split with theme packs
These two layers are orthogonal:
| Concern | Lives in | Example |
|---|---|---|
| What classes my form fields get | css_framework (this page) | <input class="form-control"> vs <input class="block w-full rounded-md…"> |
| What colors / typography those classes paint | Theme pack (Tokens) | --primary: 28 85% 55% (rust) vs 268 80% 60% (purple) |
You can pair any of the four frameworks with any of the 60+ theme
packs. pack='dracula' + css_framework='bootstrap5' is a perfectly
sensible combination — the Bootstrap bridge will render
.btn-primary in Dracula's pink.
Common pitfalls
"I switched the framework but the form still looks the same"
The css_framework setting is read at process start. Restart the dev
server (make dev) after changing it. The active value is also cached
on config — you can verify in a Python shell:
from djust.config import get_config
print(get_config().get('css_framework'))
"My Tailwind utilities don't apply"
Tailwind needs to know about the classes at build time. The shipped
tailwind.field_class value uses utilities from the default config
(block, w-full, rounded-md, border-gray-300, …). If your
project's Tailwind config is trimmed or extended differently, override
the tailwind.field_class setting to match what your build can
generate.
"The Bootstrap bridge isn't loading"
The bridge CSS is emitted by {% theme_css %} (which the shipped base
template includes in <head>). The tag lives in the theme_tags
library, so a custom base template needs {% load theme_tags %} once
before the first call:
{% load theme_tags %}
<head>
...
{% theme_css %}
</head>
Without the tag, the bridge isn't there and you're back to vanilla
Bootstrap colors. Add the tag, or copy the relevant rules from
pack_css_generator.py
into your stylesheet.
See also
- Tailwind — the brand-alias pattern for utility-class composition specifically.
- Tokens — the variables the Bootstrap bridge reads from.
- CSS Frameworks (framework guide) — the user-facing setup guide for installing Tailwind / Bootstrap with djust.
- Source:
djust.frameworks— the four adapter classes and the registry.