djust docs
Browse documentation
intermediate

CSS Frameworks

How djust abstracts over Bootstrap 4, Bootstrap 5, Tailwind, and plain HTML so the same form and component code renders correctly under each. The FrameworkAdapter contract, the class-name registry, the Bootstrap bridge, and how to author a new adapter.

On this page

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
}
ValueWhat it emitsUse when
'bootstrap5'form-control, form-select, mb-3, btn btn-primaryDefault. Modern Bootstrap.
'bootstrap4'form-control, custom-select, form-group, btn btn-primaryLegacy 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-group wrapper, custom-select for <select>, custom checkbox/radio markup.
  • Bootstrap5Adapter — mb-3 wrapper, form-select, form-check patterns.
  • TailwindAdapter — utilities directly on the input; mb-4 wrapper.
  • 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:

ConcernLives inExample
What classes my form fields getcss_framework (this page)<input class="form-control"> vs <input class="block w-full rounded-md…">
What colors / typography those classes paintTheme 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.