Home Features Docs Blog Philosophy Examples FAQ
Documentation

Components

Build reusable UI with stateless Components and interactive LiveComponents

Full documentation is on docs.djust.org

This page is a lightweight reference. The complete guide — with tutorials, theming, code examples, and more — lives on our dedicated docs site.

View on docs.djust.org

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 and djust-theming packages 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

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:

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():

<div dj-root dj-view="myapp.views.DashboardView">
    <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:

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 (if template engine fails)
    def _render_custom(self):
        return f'<span class="badge bg-{self.variant}">{self.text}</span>'

Most components only need a template. Add _render_custom() when you need framework-specific HTML (Bootstrap vs Tailwind). Add _rust_impl_class for hot-path components rendered hundreds of times per page.

Updating a Component

Call .update() to change properties without recreating the instance:

def toggle_status(self):
    new_color = "red" if self.status.color == "green" else "green"
    self.status.update(color=new_color)

Component IDs

Components get a stable ID automatically:

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:

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:

.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 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 which is already style-agnostic.

LiveComponents (Stateful)

Your First LiveComponent

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

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}
<div dj-root dj-view="myapp.views.DashboardView">
    <h2>Counter</h2>
    {{ counter.render }}
</div>

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

Descriptor-pattern auto-promotion gap

When LiveComponents are declared as class-level descriptors (the preferred pattern shown above), the framework's component-id walker (_assign_component_ids) only inspects instance-level attributes. As a result, descriptor-pattern components are NOT auto-registered in view._components today. Framework features that walk _components -- including time-travel snapshots, session-based component-state save/restore, and other introspection paths -- will silently miss descriptor-pattern components unless the view appends them manually during mount().

Workaround -- register the descriptor's instance in _components explicitly:

class MyView(LiveView):
    greeting = GreetingWidget.descriptor()  # class-level descriptor

    def mount(self, request, **kwargs):
        # Required until auto-promotion ships: makes the descriptor's
        # instance visible to time-travel snapshots and other framework
        # walkers that iterate self._components.
        self._components.append(self.greeting)

Auto-promotion is planned future framework work (tracked separately). Until it ships, document this gap in any code that mixes descriptor-pattern components with time-travel or session restore -- otherwise the component will appear to "vanish" from snapshots even though its public state is intact on the view.

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().

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:

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:

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,
        }
<!-- 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.

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:

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:

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:

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:

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:

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

Components can be registered by name for dynamic lookup:

from djust.components import register_component, get_component, list_components

# Register a custom component
register_component("status_dot", StatusDotComponent)

# Look up by name
cls = get_component("status_dot")
dot = cls(color="green")

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

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:

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
  • Inline templates get Rust-accelerated rendering. File-based templates use Django's engine.

Avoid {% elif %} in inline templates -- the Rust template engine has a known limitation. Use separate {% if %} blocks:

# Don't:
template = '{% if size == "lg" %}big{% elif size == "sm" %}small{% endif %}'

# Do:
template = '{% if size == "lg" %}big{% endif %}{% if size == "sm" %}small{% endif %}'

Components in templates render via {{ component }} -- the __str__ method calls render() automatically. For LiveComponents, use {{ component.render }} to ensure the wrapper div is included.

djust-components (Template Tag Library)

djust-components is a separate package that provides 12 style-agnostic UI components as Django template tags. Unlike the core djust.components Python classes, these use CSS custom properties for all styling -- no hardcoded Bootstrap or Tailwind classes.

Installation

pip install djust-components
# settings.py
INSTALLED_APPS = [
    "djust_components",
    # ...
]
<!-- base template -->
<link rel="stylesheet" href="{% static 'djust_components/components.css' %}">

Usage

{% 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

{% 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):

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}))
{% 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:

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
{% 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:

: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.

djust-theming

djust-theming is a production-ready theming system inspired by shadcn/ui. It provides CSS custom properties-based theming with light/dark mode, 132 built-in theme combinations, and reactive theme switching via djust LiveViews.

Installation

pip install djust-theming
# 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 (11 options)
        "preset": "blue",          # Color preset (12 options)
        "default_mode": "system",  # light, dark, or system
    }
}

Design Systems + Color Presets

Mix any design system with any color preset (11 x 12 = 132 combinations):

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

Color presets control the palette: Default, Shadcn, Blue, Green, Purple, Orange, Rose, Cyberpunk, Sunset, Forest, Ocean, Metallic

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

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

Template Integration

{% 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>

Reactive Theme Switching with LiveView

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 or export djust presets to share:

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

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

Then use theme colors in Tailwind classes:

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

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:

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 once on the first request after upgrade — 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 + djust-theming New projects. Style-agnostic, CSS custom properties, 132 theme combos, 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 alone You want pre-built template tags but will define your own --dj-* CSS variables.
Core djust.components 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 %}.

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:

{% 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=[...])).

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 — raises AssignValidationError when settings.DEBUG=True; logs a warning otherwise.
  • Type coercionstr → int, str → float, str → bool (case-insensitive true/yes/1/on; false/no/0/off/empty).
  • Enum check — values outside values=[...] raise.
  • 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.

{% 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:

@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:

{% 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.