---
title: "Components"
slug: components
section: guides
order: 4
level: beginner
description: "Build reusable UI with stateless Components and interactive LiveComponents"
---

# Components

djust provides a two-tier component system: **Component** for fast, stateless rendering and **LiveComponent** for interactive widgets with state and event handlers. Both render server-side with no JavaScript build step.

For **styling**, djust follows manifesto principle #7: *"Strong opinions on security, state, transport. Zero opinions on CSS, markup, or design."* Components output semantic HTML -- you bring your own styling via CSS custom properties, Tailwind, Bootstrap, or plain CSS. The optional [`djust.components` template tag library](#djust-components-template-tag-library) and [`djust.theming`](#djust-theming) (installed with the `djust[components]` and `djust[theming]` extras) provide a style-agnostic component library and design system powered by CSS custom properties.

## Two Types of Components

|               | Component                               | LiveComponent                            |
| ------------- | --------------------------------------- | ---------------------------------------- |
| **State**     | None                                    | Full lifecycle (mount/update/unmount)    |
| **Events**    | None                                    | `dj-click`, `dj-submit`, etc.            |
| **Rendering** | Rust-accelerated (~1-10us)              | Template-based (~50-100us)               |
| **Use for**   | Badges, icons, cards, status indicators | Tables with sorting, modals, tabs, forms |

Pick `Component` when you just need HTML output. Pick `LiveComponent` when the component needs to react to user interaction.

## Stateless Components

### Your First Component

```python
from djust.components.base import Component

class StatusDot(Component):
    template = '<span class="dot dot-{{ color }}"></span>'

    def __init__(self, color: str = "green"):
        super().__init__(color=color)
        self.color = color

    def get_context_data(self):
        return {"color": self.color}
```

Use it in a LiveView:

```python
class DashboardView(LiveView):
    template_name = "dashboard.html"

    def mount(self, request, **kwargs):
        self.status = StatusDot("green")

    def get_context_data(self, **kwargs):
        return {"status": self.status}
```

In the template, `{{ status }}` calls `__str__()` which calls `render()`:

```html
<div dj-root>
    <h1>Server Status: {{ status }}</h1>
</div>
```

### Performance Waterfall

Every `Component` tries three rendering strategies in order:

1. **Pure Rust** (~1us) -- If `_rust_impl_class` is set and the Rust extension is built
2. **Hybrid template** (~5-10us) -- If `template` is set, rendered by the Rust template engine with Django fallback
3. **Python `_render_custom()`** (~50-100us) -- Full flexibility, you write the HTML string

You don't pick one -- you define as many as you want and the fastest available wins:

```python
from djust.components.base import Component

try:
    from djust._rust import RustBadge
except ImportError:
    RustBadge = None

class Badge(Component):
    # Tier 1: Rust (if extension is built)
    _rust_impl_class = RustBadge

    # Tier 2: Hybrid template (Rust template engine + Django fallback)
    template = '<span class="badge bg-{{ variant }}">{{ text }}</span>'

    def __init__(self, text: str, variant: str = "primary"):
        super().__init__(text=text, variant=variant)
        self.text = text
        self.variant = variant

    def get_context_data(self):
        return {"text": self.text, "variant": self.variant}

    # Tier 3: Custom Python (used only when no `template` is set)
    def _render_custom(self):
        return f'<span class="badge bg-{self.variant}">{self.text}</span>'
```

Most components only need a `template`. Use `_render_custom()` instead of a `template` when you need framework-specific HTML (Bootstrap vs Tailwind). `_render_custom()` is reached only when the class has no `template`: a template that fails in the Rust engine is re-rendered by Django's engine, not by `_render_custom()`, so in the `Badge` above it never runs. Add `_rust_impl_class` for hot-path components rendered hundreds of times per page.

### Updating a Component

Write the attribute. A plain component's constructor kwargs are its state,
an attribute write goes through to that state, and change detection compares
the component by it
([ADR-033](https://github.com/djust-org/djust/blob/main/docs/adr/033-plain-component-state-and-identity.md)),
so the handler below re-renders the dot:

```python
@event_handler()
def toggle_status(self, **kwargs):
    self.status.color = "red" if self.status.color == "green" else "green"
```

`.update(color=...)` does the same for several properties at once. Only the
constructor kwargs (and any declared `fingerprint_fields`) are state; an
attribute a subclass computes for itself in `__init__` is not.

The values a component emits arrive typed — a star renders
`dj-value-value:int="4"` and the handler receives `4`, not `"4"`. When a
component has to say which instance it is, give it a `name`:

```python
self.ratings = [Rating(name=f"row-{row.pk}", value=row.stars) for row in rows]

@event_handler()
def set_rating(self, value, name=None, **kwargs):
    ...
```

Every trigger the instance renders then carries `dj-value-name="row-7"`.
(Form-field components whose constructor already takes `name`, such as
`DatePicker`, use it as the HTML field name instead, and it is not stamped
on triggers.)
Components that render their own triggers do this through
`self.event_attrs(self.event, value=...)`, which is also what a custom
component should call instead of hand-writing `dj-click="…"`.

### Component IDs

Components get a stable ID automatically:

```python
class MyView(LiveView):
    def mount(self, request, **kwargs):
        self.user_badge = Badge("Admin")
        # user_badge.id => "badge-user_badge"

        Badge("Admin", id="custom-id")
        # .id => "custom-id"
```

The waterfall: explicit `id=` parameter > auto-generated from attribute name > class name.

### Styling Components

Components are style-agnostic by default. Use CSS custom properties for themeable components:

```python
class Alert(Component):
    template = """
        <div class="dj-alert dj-alert--{{ type }}" role="alert">
            {{ message }}
        </div>
    """

    def __init__(self, message: str, type: str = "info"):
        super().__init__(message=message, type=type)
        self.message = message
        self.type = type

    def get_context_data(self):
        return {"message": self.message, "type": self.type}
```

Then style with CSS custom properties that adapt to any theme:

```css
.dj-alert {
    padding: var(--dj-spacing-3, 0.75rem);
    border-radius: var(--dj-radius, 8px);
    border: 1px solid var(--dj-border);
}
.dj-alert--success { background: var(--dj-success); color: white; }
.dj-alert--danger { background: var(--dj-danger); color: white; }
.dj-alert--info { background: var(--dj-info); color: white; }
```

This approach works with any styling system -- `djust.theming` presets, Tailwind, Bootstrap, or your own CSS. See [Styling & Theming](#djust-theming) below for the full design system.

> **Note:** The built-in components in `djust.components.ui` currently use `_render_custom()` with per-framework render methods (Bootstrap, Tailwind, Plain). This approach is being migrated toward CSS custom properties for true style independence. For new projects, prefer the [`djust.components` template tag library](#djust-components-template-tag-library), which is already style-agnostic.

## LiveComponents (Stateful)

### Your First LiveComponent

```python
from djust.components.base import LiveComponent

class CounterWidget(LiveComponent):
    template = """
        <div>
            <button dj-click="decrement" data-component-id="{{ component_id }}">-</button>
            <span>{{ count }}</span>
            <button dj-click="increment" data-component-id="{{ component_id }}">+</button>
        </div>
    """

    def mount(self, **kwargs):
        self.count = kwargs.get("initial", 0)

    @event_handler()
    def increment(self, **kwargs):
        self.count += 1
        self.trigger_update()

    @event_handler()
    def decrement(self, **kwargs):
        self.count -= 1
        self.trigger_update()

    def get_context_data(self):
        return {"count": self.count}
```

Key differences from `Component`:

- **`mount()`** is required -- set up initial state here
- **`get_context_data()`** is required -- return template variables
- **Event handlers** must be decorated with `@event_handler()` (same as LiveView). Wire them with `dj-click`, `dj-submit`, etc.
- **`data-component-id="{{ component_id }}"`** routes events to the right component instance
- **`trigger_update()`** tells the parent LiveView to re-render

### Using in a LiveView

```python
class DashboardView(LiveView):
    template_name = "dashboard.html"

    def mount(self, request, **kwargs):
        self.counter = CounterWidget(initial=10)

    def get_context_data(self, **kwargs):
        return {"counter": self.counter}
```

```html
<div dj-root>
    <h2>Counter</h2>
    {{ counter }}
</div>
```

`{{ counter }}` is the recommended spelling (see the note on component
rendering below); the markup is inserted as safe HTML on every render path.

The `component_id` is automatically set to the attribute name (`"counter"`) by the framework. No manual ID management needed.

### Class-level components

A component can also be declared **on the view class** with a `State` class
instead of `mount()`/`get_context_data()`. Each view instance gets its own
state; the attribute name is the `component_id`
([ADR-031](https://github.com/djust-org/djust/blob/main/docs/adr/031-class-level-component-rendering.md)):

```python
from djust import LiveView
from djust.components.descriptors.base import LiveComponent, TypedState
from djust.decorators import event_handler


class Tabs(LiveComponent):
    class State(TypedState):
        active: str = "overview"

    template = """
        <nav>
          <button dj-click="select" dj-value-value="overview">Overview</button>
          <button dj-click="select" dj-value-value="billing">Billing</button>
        </nav>
        <p>{{ active }}</p>
    """

    @event_handler()
    def select(self, value: str = "", **kwargs):
        self.state.active = value


class SettingsView(LiveView):
    template = "<div dj-root>{{ nav }}</div>"

    nav = Tabs(active="overview")
```

What this gives you:

- **`view.nav`** is a *bound component*: `view.nav.active` reads and writes
  this view's state (`view.nav.state` is the `State` itself), and it is
  registered in `view._components` on first access, so it is routed, captured
  in time-travel snapshots and saved/restored with the session like any other
  component, including the signed back-navigation snapshot.
- **Handlers are ordinary `@event_handler` methods** on the component; inside
  one, `self.state` is the state of the view that received the event. Clicks
  inside the rendered markup carry the `component_id` automatically because
  `{{ nav }}` wraps the output in `<div data-component-id="nav">`.
- **The State is the template context.** `{{ nav }}` renders `template` (or
  `template_name`) with the state keys plus `component_id`; there is no
  `get_context_data()` on this path. A component without a template renders
  as its state's dict repr.
- Re-rendering is cached on the state's hash, so an unchanged component costs
  one hash per render. On the LiveView path `{{ nav }}` is the rendered HTML
  string, so string filters (`{{ nav|length }}`, `|upper`) act on the markup.

The eight built-in descriptors (`Accordion`, `Tabs`, `Modal`, ... in
`djust.components.descriptors`) declare no template: they use the same
mechanism with a `Meta.event` alias registered on the view, and the
`djust_components` template tags (`{% tabs %}`, `{% accordion %}`, ...) draw
their markup from the state.

Note that `isinstance(view.nav, Tabs.State)` is `False` -- the state is
`view.nav.state`.

#### Scoped events: patching only the component

An event that changes **only** a class-level component's state does not
re-render the page
([ADR-032](https://github.com/djust-org/djust/blob/main/docs/adr/032-component-scoped-rendering.md)).
After the handler runs, the runtime compares the view's assigns before and
after; if the component's slot is the one thing that changed and the view's
template is *component-opaque* for it, only the component is rendered and its
subtree is diffed and spliced into the page's VDOM. The client receives the
same `patch` frame it always did (the debug panel and `benchmark_event` see
`timing.scope == "component"` when timing is exposed), recovery and versions
stay consistent, and nothing changes in `client.js`. Whenever any condition
fails, the page renders as before -- the scoped path is exact or not taken.

A template is component-opaque for `nav` when every reference to it is the
bare `{{ nav }}`:

```django
{# scoped: the only read is the bare variable #}
<div dj-root><h1>{{ title }}</h1>{{ nav }}</div>

{# page render: another read of the component's state #}
<div dj-root>{{ nav }} <small>{{ nav.active }}</small></div>
<div dj-root>{{ nav|length }}</div>
<div dj-root>{% if nav %}{{ nav }}{% endif %}</div>
```

Also page-rendered: any other context value that changed with the event --
a view `@property`, a `get_context_data` override or a `@computed` that
derives something from `self.nav.active` is seen because the runtime
compares the render context, key by key, against the last render before it
takes the scoped path; an unresolved `{% include %}` / `{% extends %}` (the
partial could read the state); a handler that changes any other assign in
the same event; `self._force_full_html = True`; a pending `push_event`; and
`{{ nav }}` rendered more than once or inside a `dj-update="ignore"` /
`dj-virtual` region.

### Lifecycle

```
__init__(**kwargs)
    └─> mount(**kwargs)      # Set initial state
        └─> _mounted = True

render()                     # Called on each parent re-render
    └─> get_context_data()   # Provide template vars

unmount()                    # Cleanup when removed
    └─> _mounted = False
```

### Parent-Child Communication

**Props down**: Pass data to components via constructor kwargs or `.update()`.

**Events up**: Components send events to their parent LiveView with `send_parent()`.

```python
class TodoItem(LiveComponent):
    template = """
        <div dj-click="toggle" data-component-id="{{ component_id }}">
            <input type="checkbox" {% if completed %}checked{% endif %}>
            {{ text }}
        </div>
    """

    def mount(self, **kwargs):
        self.text = kwargs.get("text", "")
        self.completed = kwargs.get("completed", False)
        self.todo_id = kwargs.get("todo_id")

    @event_handler()
    def toggle(self, **kwargs):
        self.completed = not self.completed
        # Notify parent
        self.send_parent("todo_toggled", {
            "id": self.todo_id,
            "completed": self.completed,
        })

    def get_context_data(self):
        return {"text": self.text, "completed": self.completed}
```

The parent LiveView handles it:

```python
class TodoView(LiveView):
    def mount(self, request, **kwargs):
        self.todos = [
            {"id": 1, "text": "Buy milk", "completed": False},
            {"id": 2, "text": "Write docs", "completed": True},
        ]

    def handle_component_event(self, component_id, event, data):
        if event == "todo_toggled":
            for todo in self.todos:
                if todo["id"] == data["id"]:
                    todo["completed"] = data["completed"]
```

### File-Based Templates

For complex HTML, use `template_name` instead of inline `template`:

```python
class UserCard(LiveComponent):
    template_name = "components/user_card.html"

    def mount(self, **kwargs):
        self.user = kwargs.get("user")
        self.show_actions = kwargs.get("show_actions", True)

    def get_context_data(self):
        return {
            "user": self.user,
            "show_actions": self.show_actions,
            "component_id": self.component_id,
        }
```

```html
<!-- templates/components/user_card.html -->
<div class="card" data-component-id="{{ component_id }}">
    <div class="card-body">
        <h5>{{ user.name }}</h5>
        <p>{{ user.email }}</p>
        {% if show_actions %}
        <button dj-click="edit_user"
                data-component-id="{{ component_id }}">Edit</button>
        {% endif %}
    </div>
</div>
```

Note: When using `template_name`, the component ID wrapper `<div data-component-id="...">` is **not** added automatically. Include `data-component-id="{{ component_id }}"` in your template root element.

## Built-in Components

djust ships with a library of ready-to-use components. All adapt to your CSS framework automatically.

### Browsing the catalogue

Every component has a page that renders it live, on your own server, in your
own theme. Include the theming URLs and open `/theme/components/`:

```python
# urls.py
path("theme/", include("djust.theming.urls")),
```

Each page shows the component running, the option chips you can flip to see
the variants, the two files you would write to use it (the view and the
template line), its parameters, the events it sends to your view, its
accessibility rules and where to override its styles. `/` focuses the search
box. The pages are LiveViews, so the previews are the real component on a
real server rather than a screenshot.

The catalogue is gated like the theme gallery: `DEBUG`, or a staff user, or
`DJUST_THEMING_GALLERY_PUBLIC = True` for a site that publishes it. The theme
presets live beside it at `/theme/themes/`.

Prefer to read rather than click? The same components are written up at
[docs.djust.org/reference/components/](https://docs.djust.org/reference/components/),
generated from the same registry, and every catalogue page links to its
entry there.

Link to the catalogue with a plain `href`, never `dj-navigate`: these pages
bring their own stylesheet and script in `<head>`, and a `dj-root`-only SPA
swap would drop the reader on unstyled markup
([why](navigation.md#when-not-to-use-dj-navigate)).

**Hosting the catalogue inside your own site.** The pages render inside
`djust_theming/catalogue/_document.html`. Ship a template at that path from an
app listed *before* `djust.theming` in `INSTALLED_APPS` and yours wins: put
your own navigation in it, include
`{% include "djust_theming/catalogue/_assets.html" %}` in the `<head>`, load
`djust_theming/js/catalogue.js`, and render `{% block body %}` — that block is
where the catalogue puts its mount root, sidebar and content, all of which
must stay inside it. Everything else in the file is the framework's own
chrome, so replacing the file replaces the topbar too and your page renders
one navigation rather than two.

### UI Components

| Component           | Type          | Description                                                                                                                                                                                                                         |
| ------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AlertComponent`    | LiveComponent | Dismissible alerts with `.show()` / `.dismiss()`                                                                                                                                                                                    |
| `BadgeComponent`    | LiveComponent | Interactive badges                                                                                                                                                                                                                  |
| `Badge`             | Component     | Stateless badges (faster)                                                                                                                                                                                                           |
| `ButtonComponent`   | LiveComponent | Buttons with disable/enable                                                                                                                                                                                                         |
| `Button`            | Component     | Stateless buttons                                                                                                                                                                                                                   |
| `CardComponent`     | LiveComponent | Cards with dynamic content                                                                                                                                                                                                          |
| `Card`              | Component     | Stateless cards                                                                                                                                                                                                                     |
| `DropdownComponent` | LiveComponent | Dropdown menus                                                                                                                                                                                                                      |
| `ModalComponent`    | LiveComponent | Show/hide modals programmatically                                                                                                                                                                                                   |
| `ProgressComponent` | LiveComponent | Progress bars with `.set_value()` / `.increment()`                                                                                                                                                                                  |
| `SpinnerComponent`  | LiveComponent | Loading indicators with `.show()` / `.hide()`                                                                                                                                                                                       |
| More stateless:     | Component     | `Accordion`, `Avatar`, `Breadcrumb`, `ButtonGroup`, `Checkbox`, `Divider`, `Icon`, `Input`, `ListGroup`, `NavBar`, `Offcanvas`, `Pagination`, `Radio`, `Range`, `Select`, `Switch`, `Table`, `Tabs`, `TextArea`, `Toast`, `Tooltip` |

### Layout Components

| Component         | Type          | Description                              |
| ----------------- | ------------- | ---------------------------------------- |
| `TabsComponent`   | LiveComponent | Tabbed navigation with `.activate_tab()` |
| `NavbarComponent` | LiveComponent | Navigation bars with `.set_active()`     |

### Data Components

| Component             | Type          | Description                            |
| --------------------- | ------------- | -------------------------------------- |
| `TableComponent`      | LiveComponent | Sortable data tables with `.sort_by()` |
| `PaginationComponent` | LiveComponent | Page navigation                        |

### Form Components

| Component          | Type          | Description                                      |
| ------------------ | ------------- | ------------------------------------------------ |
| `ForeignKeySelect` | LiveComponent | Django ForeignKey field with search/autocomplete |
| `ManyToManySelect` | LiveComponent | Django M2M field with checkboxes or multi-select |

### Usage Examples

**Alert:**

```python
def mount(self, request, **kwargs):
    self.alert = AlertComponent(
        message="Changes saved!",
        type="success",
        dismissible=True,
    )

@event_handler()
def save(self, **kwargs):
    # ... save logic ...
    self.alert.show("Changes saved!", "success")

@event_handler()
def on_error(self, **kwargs):
    self.alert.show("Something went wrong", "danger")
```

**Modal:**

```python
def mount(self, request, **kwargs):
    self.confirm_modal = ModalComponent(
        title="Confirm Delete",
        body="This action cannot be undone.",
        show=False,
        size="md",  # sm, md, lg, xl
    )

@event_handler()
def delete_clicked(self, **kwargs):
    self.confirm_modal.show()

@event_handler()
def dismiss(self, **kwargs):
    self.confirm_modal.hide()
```

**Tabs:**

```python
from djust.components.layout import TabsComponent

def mount(self, request, **kwargs):
    self.tabs = TabsComponent(
        tabs=[
            {"id": "overview", "label": "Overview", "content": "..."},
            {"id": "settings", "label": "Settings", "content": "..."},
            {"id": "logs", "label": "Logs", "content": "...", "badge": "3"},
        ],
        active="overview",
        variant="tabs",  # "tabs" or "pills"
    )
```

**Data Table:**

```python
from djust.components import TableComponent

def mount(self, request, **kwargs):
    self.users_table = TableComponent(
        columns=[
            {"key": "name", "label": "Name", "sortable": True},
            {"key": "email", "label": "Email"},
            {"key": "role", "label": "Role", "sortable": True},
        ],
        rows=list(User.objects.values("name", "email", "role")),
        striped=True,
        hoverable=True,
    )
```

**ForeignKey Select:**

```python
from djust.components.forms import ForeignKeySelect

def mount(self, request, **kwargs):
    self.category = ForeignKeySelect(
        name="category",
        queryset=Category.objects.all(),
        label_field="name",
        label="Category",
        required=True,
        searchable=True,
        search_fields=["name"],
    )
```

## Component Registry

LiveComponent classes can be registered by name for dynamic lookup (`register_component` rejects plain `Component` subclasses with `TypeError`):

```python
from djust.components import register_component, get_component, list_components

# Register a custom LiveComponent (CounterWidget from above)
register_component("counter", CounterWidget)

# Look up by name
cls = get_component("counter")

# List all registered components
for name, cls in list_components().items():
    print(f"{name}: {cls.__name__}")
```

All built-in LiveComponents are auto-registered: `alert`, `badge`, `button`, `card`, `dropdown`, `modal`, `progress`, `spinner`, `tabs`, `table`, `pagination`.

## Writing Custom Components

### Stateless: Extend `Component`

Use when you need fast rendering with no interaction:

```python
from djust.components.base import Component

class PriceBadge(Component):
    template = """
        <span class="price {% if on_sale %}price--sale{% endif %}">
            {% if on_sale %}<s>{{ original }}</s> {% endif %}
            {{ current }}
        </span>
    """

    def __init__(self, current: str, original: str = "", on_sale: bool = False):
        super().__init__(current=current, original=original, on_sale=on_sale)
        self.current = current
        self.original = original
        self.on_sale = on_sale

    def get_context_data(self):
        return {
            "current": self.current,
            "original": self.original,
            "on_sale": self.on_sale,
        }
```

### Stateful: Extend `LiveComponent`

Use when the component needs to handle events or manage state:

```python
from djust.components.base import LiveComponent

class SearchBox(LiveComponent):
    template = """
        <div data-component-id="{{ component_id }}">
            <input type="text" dj-input="on_search"
                   data-component-id="{{ component_id }}"
                   value="{{ query }}" placeholder="Search...">
            {% if loading %}
                <span class="spinner-border spinner-border-sm"></span>
            {% endif %}
            <ul>
            {% for result in results %}
                <li dj-click="select_result"
                    data-component-id="{{ component_id }}"
                    data-id="{{ result.id }}">
                    {{ result.name }}
                </li>
            {% endfor %}
            </ul>
        </div>
    """

    def mount(self, **kwargs):
        self.query = ""
        self.results = []
        self.loading = False
        self.search_fn = kwargs.get("search_fn")  # Callable for searching

    @event_handler()
    def on_search(self, value="", **kwargs):
        self.query = value
        if len(value) >= 2 and self.search_fn:
            self.loading = True
            self.results = self.search_fn(value)
            self.loading = False
        else:
            self.results = []
        self.trigger_update()

    @event_handler()
    def select_result(self, id=None, **kwargs):
        self.send_parent("result_selected", {"id": id})

    def get_context_data(self):
        return {
            "query": self.query,
            "results": self.results,
            "loading": self.loading,
        }
```

### Checklist

When building a custom component:

- [ ] `@event_handler()` on every method wired to a `dj-*` event
- [ ] `**kwargs` in every event handler signature
- [ ] `data-component-id="{{ component_id }}"` on every element with `dj-*` events
- [ ] `trigger_update()` after state changes that should re-render
- [ ] `send_parent()` for events the parent needs to know about
- [ ] `get_context_data()` returns all variables used in the template
- [ ] `mount()` initializes all state -- don't rely on class-level defaults for mutable objects

## Template Tips

**Inline vs file-based templates:**

- Use inline `template = "..."` for small components (< 20 lines of HTML)
- Use `template_name = "components/my_widget.html"` for complex HTML
- Both inline `template` and file-based `template_name` render through djust's Rust engine. A `template_name` that uses `{% extends %}` falls back to Django's engine.
- `{% if %}` / `{% elif %}` / `{% else %}` chains are supported in both.

**Components in templates render via `{{ component }}`** -- the `__str__` method calls `render()` automatically, and this is the canonical spelling for both `Component` and `LiveComponent`; every example in these docs uses it ([#2634](https://github.com/djust-org/djust/issues/2634)). `{{ component.render }}` still resolves (it's the same underlying call) but isn't preferred: it depends on Django's nullary auto-call, which `LIVEVIEW_CONFIG['template_auto_call'] = False` switches off -- `{{ component }}` then still renders, while `{{ component.render }}` prints a bound-method repr. As of [#2503](https://github.com/djust-org/djust/issues/2503), the two spellings render **identical output** on every render path -- verified directly, not assumed.

> A component's markup is inserted **unescaped on all four render paths** (`DjustTemplateBackend`, the two standalone `render_template` entry points, and the LiveView path) -- no `|safe` needed. An earlier version of this note said the opposite while [#2501](https://github.com/djust-org/djust/issues/2501) was open; it is closed, and the four paths were re-measured for ADR-031.

## djust-components (Template Tag Library)

The `djust.components` app ships a library of style-agnostic UI components as Django template tags. Unlike the Python component classes in `djust.components.ui`, these use CSS custom properties for all styling -- no hardcoded Bootstrap or Tailwind classes.

### Installation

```bash
pip install "djust[components]"
```

```python
# settings.py
INSTALLED_APPS = [
    "djust.components",
    # ...
]
```

```html
<!-- base template -->
<link rel="stylesheet" href="{% static 'djust_components/components.css' %}">
```

### Usage

```html
{% load djust_components %}

{% modal id="confirm" title="Are you sure?" open=modal_open %}
  <p>This action cannot be undone.</p>
  <button dj-click="confirm_delete">Delete</button>
  <button dj-click="close_modal">Cancel</button>
{% endmodal %}

{% tabs id="settings" active=active_tab event="set_tab" %}
  {% tab id="general" label="General" %}
    General settings content
  {% endtab %}
  {% tab id="security" label="Security" icon="🔒" %}
    Security settings content
  {% endtab %}
{% endtabs %}

{% card title="Dashboard" variant="elevated" %}
  Card content here
{% endcard %}

{% badge label="Online" status="online" pulse=True %}

{% progress value=75 label="Upload" color="success" %}

{% data_table rows=rows columns=columns sort_by=sort_by sort_desc=sort_desc %}

{% toast_container toasts dismiss_event="dismiss_toast" %}
```

### Available Components

| Tag                                        | Description                          |
| ------------------------------------------ | ------------------------------------ |
| `{% modal %}`                              | Overlay dialog with backdrop blur    |
| `{% tabs %}` / `{% tab %}`                 | Content switching with active state  |
| `{% accordion %}` / `{% accordion_item %}` | Expandable sections                  |
| `{% dropdown %}`                           | Toggle menu                          |
| `{% toast_container %}`                    | Server-push notifications            |
| `{% tooltip %}`                            | Hover tooltip                        |
| `{% progress %}`                           | Animated progress bar                |
| `{% badge %}`                              | Status indicator with optional pulse |
| `{% card %}`                               | Content container                    |
| `{% data_table %}`                         | Sortable table with pagination       |
| `{% pagination %}`                         | Page navigation                      |
| `{% avatar %}`                             | User avatar with initials fallback   |

#### Styles for `{% badge %}`, `{% avatar %}`, `{% progress %}` and `{% toast_container %}`

These tags render BEM class names, styled in
`djust_components/components.css` (the file the `<link>` above loads, and the
one `{% theme_head %}` links whenever `djust.components` is installed). The
rules are on the active theme's tokens, so the components follow light and
dark mode. The status colour goes on the fill, border, dot or bar, and every
label stays on `--foreground`, so no label's contrast depends on a status hue.

```html
<span class="dj-badge dj-badge--error">
  <span class="dj-badge__dot dj-badge__dot--pulse" aria-hidden="true"></span>
  <span class="sr-only">Status: error</span>
  <span class="dj-badge__label">Failed</span>
</span>
```

| Tag | Classes it renders |
| --- | --- |
| `{% badge %}` | `dj-badge`, `dj-badge--<status>` (`default`, `online`, `offline`, `warning`, `error`, `info`), `dj-badge__dot`, `dj-badge__dot--pulse`, `dj-badge__label` |
| `{% avatar %}` | `dj-avatar`, `dj-avatar--<size>`, `dj-avatar__img`, `dj-avatar__initials`, `dj-avatar__status`, `dj-avatar__status--<status>` |
| `{% progress %}` | `dj-progress`, `dj-progress--<size>`, `dj-progress__header`, `dj-progress__label`, `dj-progress__value`, `dj-progress__track`, `dj-progress__fill`, `dj-progress__fill--<color>` |
| `{% toast_container %}` | `dj-toast-container`, `dj-toast`, `dj-toast--<type>`, `dj-toast__icon`, `dj-toast__message`, `dj-toast__dismiss` |

To restyle one, write a rule for the same class after the stylesheet. The
similarly named `.badge-online` / `.badge-pulse` rules belong to other badge
markup, and `.dj-badge-danger` (single dash) to the `Badge` Python class.
The `Toast` Python class renders single-dash names (`dj-toast-<type>`,
`dj-toast-message`, `dj-toast-dismiss`), styled in the same file, which also
reads the `--dj-toast-*` properties its docstring lists (`--dj-toast-bg`,
`--dj-toast-success-border`, …), each falling back to a theme token.
`{% toast_container %}` toasts read only the six base properties
(`--dj-toast-bg`, `-fg`, `-border`, `-radius`, `-padding`, `-shadow`), not
the per-type ones.

### `{% data_table %}` row-level navigation (#1111)

Two ways to make whole rows clickable for navigation. Both render
`role="button"`, `tabindex="0"`, and `cursor:pointer` on every row, and
both respect Enter / Space for keyboard activation. Clicks inside
nested interactive descendants (`<a>`, `<button>`, `<input>`,
`<label>`, `<select>`, `<textarea>`) are short-circuited so they
**never** trigger row navigation.

**Option B — `row_click_event` (preferred, LiveView-idiomatic)**: each
`<tr>` fires a djust event with `data-value=row[row_click_value_key]`.
Your `@event_handler` receives the row's value via `**kwargs` and can
`self.redirect(...)` (or do anything else):

```python
from djust import LiveView
from djust.decorators import event_handler
from django.urls import reverse

class ClaimsListView(LiveView):
    template_name = "claims/list.html"

    def mount(self, request, **kwargs):
        self.rows = list(Claim.objects.values("id", "claim_number", "status"))
        self.columns = [
            {"key": "claim_number", "label": "Claim #"},
            {"key": "status", "label": "Status"},
        ]

    @event_handler()
    def open_claim(self, value: str = "", **kwargs):
        self.redirect(reverse("claims:detail", kwargs={"claim_id": value}))
```

```django
{% data_table rows=rows columns=columns
              row_click_event="open_claim"
              row_click_value_key="id" %}
```

`row_click_value_key` defaults to `"id"`; override for slug-based
routing (e.g. `"uuid"`, `"slug"`).

**Option A — `row_url` (static-URL fallback)**: each `<tr>` gets
`data-href` from a row dict key; the component JS module navigates via
`window.location.assign(href)` on click / Enter / Space. Use when the
target URL is computed once in the view and doesn't need a server
round-trip:

```python
def get_context_data(self, **kwargs):
    ctx = super().get_context_data(**kwargs)
    ctx["rows"] = [
        {"claim_number": "C-001", "claim_url": reverse("claims:detail", args=[1])},
    ]
    ctx["columns"] = [{"key": "claim_number", "label": "Claim #"}]
    return ctx
```

```django
{% data_table rows=rows columns=columns row_url="claim_url" %}
```

`row_click_event` takes precedence when both are set.

**Accessibility**: row-clickable `<tr>`s are focusable (`tabindex="0"`),
announce as buttons (`role="button"`), and activate on Enter or Space.
Screen reader users get the same affordance as mouse / touch users.

**CSP-strict friendly**: no inline event handlers, no `eval`, no nonce
plumbing required. Both options work under
`Content-Security-Policy: script-src 'self'` once the component JS
module is served as a static asset (no `'unsafe-inline'`).

**Composition with `selectable=True`**: per-row checkbox cells are
`<input>` elements, which are in the nested-control guard's protected
list. Clicking the checkbox fires the existing `select_event` and does
**not** also fire row navigation.

**Composition with cell-level link columns (#1110)**: cell `<a>` tags
also "win" — clicking a cell link follows the link without firing the
row event.

**Static-URL safety**: Option A's `data-href` value is regex-validated
(`/^(https?:|\/|\.)/`) before navigation, so a hostile `javascript:`
URI cannot execute even if it sneaks into the row dict. Always prefer
URLs computed from `reverse()` in your view; never assign
user-controlled strings to `row_url` data.

### Customization

All components use CSS custom properties. Override them to match any theme:

```css
:root {
  --dj-primary: #6366f1;
  --dj-success: #22c55e;
  --dj-warning: #eab308;
  --dj-danger: #ef4444;
  --dj-text: #e2e8f0;
  --dj-bg: #0f172a;
  --dj-bg-subtle: #1e293b;
  --dj-border: rgba(99, 102, 241, 0.15);
  --dj-radius: 8px;
}
```

This is what makes them style-agnostic: change the variables, and every component adapts. Works standalone or with `djust.theming` for full design system support.

#### Styles for the `Alert`, `Progress` and `Avatar` classes

The `djust.components` classes `Alert`, `Progress` and `Avatar` are styled in
the same `djust_components/components.css`, on the theme's tokens. Each reads
its own custom properties first, so you can restyle one component without
touching the theme:

| Class (`from djust.components import …`) | Classes it renders | Custom properties it reads |
| --- | --- | --- |
| `Alert` | `dj-alert`, `dj-alert-<variant>`, `dj-alert-dismissible`, `dj-alert-icon`, `dj-alert-message`, `dj-alert-dismiss` | `--dj-alert-{bg,fg,border,radius,padding}`, `--dj-alert-<variant>-{bg,fg,border}` |
| `Progress` | `dj-progress`, `dj-progress-<variant>`, `dj-progress-<size>`, `dj-progress-label`, `dj-progress-track`, `dj-progress-bar`, `dj-progress-value` | `--dj-progress-{bg,bar-bg,radius,height}`, `--dj-progress-<variant>-bg` |
| `Avatar` | `dj-avatar`, `dj-avatar-<size>`, `dj-avatar-img`, `dj-avatar-initials`, `dj-avatar-status`, `dj-avatar-status-<status>` | `--dj-avatar-size`, set on the avatar element itself (its size class sets the property there, so a value on a parent has no effect) |

The `{% theme_alert %}` / `{% theme_progress %}` / `{% theme_avatar %}` tags
from `djust.theming` render the same components with the theming package's
own class names.

## djust-theming

`djust.theming` is a production-ready theming system inspired by shadcn/ui, shipped in core. It provides CSS custom properties-based theming with light/dark mode, 12 design systems × 68 color presets, and reactive theme switching via djust LiveViews.

### Installation

```bash
pip install "djust[theming]"
```

```python
# settings.py
INSTALLED_APPS = [
    "djust.theming",
    # ...
]

TEMPLATES = [{
    "OPTIONS": {
        "context_processors": [
            "djust.theming.context_processors.theme_context",
        ],
    },
}]

# Choose a design system + color preset
LIVEVIEW_CONFIG = {
    "theme": {
        "theme": "material",       # Design system (12 options)
        "preset": "blue",          # Color preset (68 options)
        "default_mode": "system",  # light, dark, or system
    }
}
```

### Design Systems + Color Presets

Mix any design system with any color preset (12 design systems × 68 color presets):

**Design systems** control typography, spacing, radius, shadows, and animations:
Material, iOS, Fluent, Minimalist, Playful, Corporate, Retro, Elegant, Neo-Brutalist, Organic, Dense, djust

**Color presets** control the palette. A few of the 68: Default, Shadcn, Blue, Green, Purple, Orange, Rose, Cyberpunk, Forest, Nord, Dracula, Ocean Deep, Sunrise. Run `python manage.py djust_theme list-presets` for the full list.

```python
# Material Design + Cyberpunk colors
LIVEVIEW_CONFIG = {"theme": {"theme": "material", "preset": "cyberpunk"}}

# iOS + Forest green palette
LIVEVIEW_CONFIG = {"theme": {"theme": "ios", "preset": "forest"}}
```

### Template Integration

```html
{% load theme_tags static %}
<!DOCTYPE html>
<html>
<head>
    {% theme_head link_css=True %}
    <link href="{% static 'djust_theming/css/base.css' %}" rel="stylesheet">
</head>
<body>
    {% theme_switcher %}          {# Full theme switcher UI #}
    {% theme_mode_toggle %}       {# Light/dark toggle #}
    {% theme_preset_selector %}   {# Preset picker #}

    <div class="card">
        <div class="card-header">Dashboard</div>
        <div class="card-body">
            <button class="btn btn-primary">Get Started</button>
        </div>
    </div>
</body>
</html>
```

The `{% theme_X %}` tags above work in **both** template engines — Django's
and djust's Rust engine (the one that renders LiveView templates). They are
also available as context-string variables (`{{ theme_head }}`,
`{{ theme_switcher }}`, `{{ theme_panel }}`, `{{ theme_mode_toggle }}`,
`{{ theme_preset_selector }}`) for the default-args case. Prefer the
`{{ ... }}` form when a tag appears multiple times on a page (it pre-renders
once per request); use the `{% theme_X %}` tag form when you need
customization-with-args, e.g. `{% theme_panel show_packs=False %}` or
`{% theme_preset_selector layout="grid" %}`.

### Reactive Theme Switching with LiveView

```python
from djust import LiveView
from djust.theming import ThemeMixin

class DashboardView(ThemeMixin, LiveView):
    template_name = "dashboard.html"

    def mount(self, request, **kwargs):
        super().mount(request, **kwargs)
        # theme_head, theme_switcher, theme_preset, theme_mode
        # are automatically available in the template context
```

Theme changes happen instantly over WebSocket -- no page reload.

### shadcn/ui Compatibility

Import themes from [themes.shadcn.com](https://themes.shadcn.com) or export djust presets to share:

```bash
python manage.py djust_theme shadcn-import my-theme.json
python manage.py djust_theme shadcn-export --preset blue --output blue-theme.json
```

### Tailwind Integration

```bash
python manage.py djust_theme tailwind-config --preset blue --output tailwind.config.js
```

Then use theme colors in Tailwind classes:

```html
<button class="bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-md">
  Primary Button
</button>
```

### Cross-project cookie isolation (`cookie_namespace`)

If you run two or more djust projects on the same domain (e.g.
`localhost:8001`, `localhost:8002`, etc.), browsers scope cookies by domain
only — *not* by port. Without isolation, the four theming cookies
(`djust_theme`, `djust_theme_preset`, `djust_theme_pack`,
`djust_theme_layout`) leak across projects, so picking the "Cyberpunk" pack
in project A pins it for project B too.

For sites without a user-facing theme switcher, the simpler fix is
`enable_client_override: False` (introduced in v0.8.2 / #1013), which makes
the server ignore client cookies entirely.

For sites *with* a user-facing switcher — where cookie writes must stay on —
set a per-project namespace:

```python
LIVEVIEW_CONFIG = {
    "theme": {
        "cookie_namespace": "djust_org",   # any unique-per-project string
    },
}
```

When set, theming cookies are read and written under
`<ns>_djust_theme`, `<ns>_djust_theme_preset`, `<ns>_djust_theme_pack`, and
`<ns>_djust_theme_layout`, so each project gets its own slot in the shared
cookie jar.

The read path tries the namespaced cookie first and falls back to the legacy
unprefixed cookie whenever the namespaced one is absent (until the next theme
write creates it) — users keep their existing theme choice. After the user clicks the switcher (or any other path
that calls `setPreset` / `setPack` / `setTheme` / `setLayout`), only the
namespaced cookie is written.

When `cookie_namespace` is unset (default), the unprefixed names are used —
existing deployments don't lose their theme on upgrade.

## Choosing a Styling Approach

| Approach                                 | When to Use                                                                                                                           |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **`djust.components` tags + `djust.theming`** | New projects. Style-agnostic, CSS custom properties, 12 design systems × 68 color presets, shadcn/ui compatible. **Recommended.** |
| **`djust.theming` alone**                | You want the design system and theme switching but prefer to write your own component HTML.                                           |
| **`djust.components` tags alone**        | You want pre-built template tags but will define your own `--dj-*` CSS variables.                                                     |
| **Python component classes**             | You need Rust-accelerated rendering for high-frequency components (100+ per page), or need programmatic component creation in Python. |
| **Plain HTML**                           | You want full control. Use `dj-click`, `dj-submit` etc. directly on your own markup. djust has zero opinions on your HTML structure.  |

## Function Components (v0.5.0+)

For the ~80% of UI pieces that are stateless — buttons, cards, badges, icons, alert boxes — function components close the gap between raw HTML and a full `LiveComponent`. They are plain Python functions registered via the `@component` decorator and invokable from templates with `{% call %}` or `{% component %}`.

```python
from djust import component, Assign

@component
def badge(assigns):
    variant = assigns.get("variant", "default")
    return f'<span class="badge bg-{variant}">{assigns["children"]}</span>'

@component(
    name="primary_button",
    assigns=[
        Assign("label", type=str, required=True),
        Assign("size", type=str, default="md", values=["sm", "md", "lg"]),
    ],
)
def button(assigns):
    return f'<button class="btn btn-{assigns["size"]}">{assigns["label"]}</button>'
```

Usage in templates:

```django
{% call "badge" variant="primary" %}New{% endcall %}

{% component "primary_button" label="Save" size="lg" %}{% endcomponent %}
```

Both tags are synonyms — pick whichever reads best at the call site. The body of the tag (everything between `{% call %}` / `{% endcall %}`) becomes `assigns["children"]` *and* `assigns["inner_block"]` (Phoenix parity). The body **always** wins over any `children=`/`inner_block=` kwarg passed at the call site — the block body is authoritative.

> **⚠ Escape user-controlled strings.** The examples above f-string-interpolate `variant` / `label` directly into HTML. That's safe when the values come from your own code (literals, enum values), but if a kwarg originates from user input — e.g. `{% call badge variant=user_selected_variant %}` where `user_selected_variant` is a form field — you must escape it. Prefer `html.escape(variant)` or use `format_html(...)` from `django.utils.html`. The function's return value is treated as trusted HTML by the template engine (no auto-escape), which is what makes components expressive — but also means XSS is the component author's responsibility, not the template engine's.

### Declarative Assigns

Declare the expected inputs with `Assign(...)` to get runtime validation, type coercion, and self-documenting components. Available on both `LiveComponent` subclasses (via class attribute) and function components (via `@component(assigns=[...])`).

`assigns` on a `LiveComponent` are validated whenever the class is constructed. `slots` are different: they are enforced only when the class is registered with `@component` and invoked through `{% call %}` / `{% component %}`. A `Card(title=...)` built in `mount()` never has its `required=True` slot checked. When invoked through `{% call %}`, the slot content reaches the instance as `self._slots` / `self._children`, which your `get_context_data()` must expose to the template itself.

```python
from djust import LiveComponent, Assign, Slot

class Card(LiveComponent):
    template_name = "components/card.html"
    assigns = [
        Assign("title", type=str, required=True),
        Assign("variant", type=str, default="default",
               values=["default", "primary", "danger"]),
        Assign("padding", type=int, default=4),   # str "8" -> int 8 auto-coercion
        Assign("dismissable", type=bool, default=False),  # "true"/"yes"/"1" accepted
    ]
    slots = [Slot("inner_block", required=True)]
```

Validation behaviour:

- **Required missing**, **bad coercion**, **value outside `values=[...]`** — all are validation failures.
- **Type coercion** — `str → int`, `str → float`, `str → bool` (case-insensitive `true`/`yes`/`1`/`on`; `false`/`no`/`0`/`off`/empty).
- **Where failures go** — for a `LiveComponent`, any validation failure raises `AssignValidationError` when `settings.DEBUG=True` and logs a warning otherwise (the raw kwargs are kept). For a function component invoked with `{% call %}` / `{% component %}`, any failure raises `RuntimeError("Component '<name>' validation failed: …")` in every mode.
- **Inheritance** — child-class `assigns` extend parent's; same-name entries override.

### Named Slots

Pass multiple named content blocks (with attributes) into a component. Perfect for tables where the parent defines columns, or any layout where content has distinct regions.

```django
{% call "card" %}
  {% slot header label="Dashboard" %}
    <h3>Today's Metrics</h3>
  {% endslot %}
  {% slot footer %}
    <button>Close</button>
  {% endslot %}
  Main body content here.
{% endcall %}
```

Inside the component, slots are available as a dict:

```python
@component
def card(assigns):
    header = assigns["slots"].get("header", [{"content": ""}])[0]["content"]
    footer = assigns["slots"].get("footer", [{"content": ""}])[0]["content"]
    body = assigns["children"]  # Non-slot content
    return f"<div class='card'><header>{header}</header>{body}<footer>{footer}</footer></div>"
```

`assigns["slots"]` is `{name: [slot_dict, ...]}` where each `slot_dict` carries `{"attrs": {...}, "content": "..."}`. Multiple same-name slots collect into a list. If you declare the slot, declare it `Slot("col", multiple=True)`: a declared slot is single by default, and repeating it raises.

```django
{% slot col label="Name" %}{{ row.name }}{% endslot %}
{% slot col label="Email" %}{{ row.email }}{% endslot %}
{% slot col label="Joined" %}{{ row.joined }}{% endslot %}
```

yields `assigns["slots"]["col"]` as a 3-element list. The inline tag `{% render_slot slots.col.0 %}` emits the content of a slot at a given path — useful when you need to reference a slot inside a rendered loop. `{% render_slot slots.col.0.content %}` names the body directly and emits the same markup; both are the parent's already-rendered output, so both are emitted live rather than escaped a second time. Any OTHER path — a plain context variable, or a `content` key on a dict that is not a slot entry — is escaped, because nothing has vouched for it.

## Markdown editor

See the [Markdown Editor guide](markdown-editor.md) for optional Visual/Markdown editing,
native form integration, asset loading, theme variables and editing limitations.
