---
title: "CSS Frameworks"
slug: theming-css-framework
section: theming
order: 25
level: intermediate
description: "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."
---

# 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`:

```python
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:

```python
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.

```python
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:

```python
'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:

```python
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:

```python
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:

```css
/* 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](/theming/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:

```python
# 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
```

```python
# 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',
    },
}
```

```python
# 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](/theming/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:

```python
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:

```django
{% 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`](https://github.com/djust-org/djust/blob/main/python/djust/theming/pack_css_generator.py)
into your stylesheet.

## See also

- **[Tailwind](/theming/tailwind/)** — the brand-alias pattern for
  utility-class composition specifically.
- **[Tokens](/theming/tokens/)** — the variables the Bootstrap bridge
  reads from.
- **[CSS Frameworks (framework guide)](/guides/css-frameworks/)** —
  the user-facing setup guide for installing Tailwind / Bootstrap with
  djust.
- **Source**:
  [`djust.frameworks`](https://github.com/djust-org/djust/blob/main/python/djust/frameworks.py)
  — the four adapter classes and the registry.
