---
title: "Runtime Switching"
slug: theming-runtime-switching
section: theming
order: 60
level: intermediate
description: "How users switch packs and modes at runtime — the {% theme_panel %} widget, programmatic API, session persistence, and per-domain cookie scoping."
---

# Runtime Switching

The pack chosen in `LIVEVIEW_CONFIG['theme']` is a *default*. Users — or
your code — can switch to any registered pack at runtime, and the choice
persists across requests in the user's session.

## The `{% theme_panel %}` template tag

The drop-in user-facing switcher is a gear-icon dropdown with a mode
toggle and a pack picker. Render it anywhere in your layout (typically
in the masthead next to other actions):

```django
{% load djust_theming %}

<header class="masthead">
    <a href="/" class="logo">my-app</a>
    <nav>...</nav>
    {% theme_panel %}
</header>
```

What it includes:

- A **mode toggle** — light / dark, gated on
  `LIVEVIEW_CONFIG['theme']['enable_dark_mode']`.
- A **pack picker** — every registered pack, grouped by category
  (professional / playful / minimal / bold / elegant / retro).
- A **persist toggle** — if `persist_in_session=True`, the user's choice
  saves to the session cookie immediately. Otherwise the choice lasts
  for the current page only.

The widget is a Django context-processor render, not a LiveView, so it
ships HTML on first paint — no flash of wrong theme.

## Programmatic switching

For "make this the active pack" actions (e.g. theme-preview pages,
admin overrides, API-driven theme changes):

```python
from djust.theming import set_active_pack, set_active_mode

class HeaderView(LiveView):
    @event_handler
    def use_dracula(self):
        set_active_pack(self.request, 'dracula')

    @event_handler
    def toggle_mode(self):
        current = get_active_mode(self.request)   # 'light' | 'dark'
        set_active_mode(self.request, 'dark' if current == 'light' else 'light')
```

Both functions write to the session immediately if `persist_in_session`
is true, and the next render uses the new active state.

## Reading the active state

```python
from djust.theming import get_active_pack, get_active_mode

def my_view(request):
    pack = get_active_pack(request)       # ThemePack
    mode = get_active_mode(request)       # 'light' | 'dark'
    print(pack.name, pack.preset.display_name, mode)
```

In templates, the same is exposed via the context processor:

```django
Active pack: {{ theme_pack }}
Mode: {{ theme_resolved_mode }}
```

## Session persistence

If `LIVEVIEW_CONFIG['theme']['persist_in_session'] = True` (the default),
the user's pack and mode are written to the session and restored on
every subsequent visit. Logged-in users can carry their preference
across devices via Django session storage; anonymous users get a
session cookie.

To clear a user's saved choice (e.g. on logout):

```python
from djust.theming import reset_to_defaults
reset_to_defaults(request)
```

## Cookie scoping for shared domains (v0.9.0+)

Browsers scope cookies by domain — *not* by port. If you run multiple
djust projects on `localhost:8001` / `localhost:8002` / `localhost:8003`
during development, their theme cookies will overwrite each other unless
each project sets a unique `cookie_namespace`:

```python
# settings.py for docs.djust.org
LIVEVIEW_CONFIG = {
    'theme': {
        'pack': 'docs',
        'cookie_namespace': 'docs',     # cookie name: djust_theme__docs
    },
}

# settings.py for djust.org
LIVEVIEW_CONFIG = {
    'theme': {
        'pack': 'djust',
        'cookie_namespace': 'marketing', # cookie name: djust_theme__marketing
    },
}
```

In production this is rarely needed — each project gets its own domain.
It's primarily a local-dev quality-of-life feature.

## Anti-FOUC (Flash of Unstyled Content)

The theme panel persists the choice in a session cookie. On the next
request, the server renders with the correct pack from the start — no
JS-flash. The base template should also include a small inline `<script>`
in `<head>` that synchronizes any localStorage-based pack/mode hints
djust's shipped layouts include this; if you author your own base template,
copy the pattern from
[`djust/templates/djust_theming/_anti_fouc.html`](https://github.com/djust-org/djust/blob/main/python/djust/theming/templates/djust_theming/_anti_fouc.html).

## Cache-busting with `{% theme_css_link %}`

Theme packs ship CSS with long-lived cache headers — a feature for repeat
visits, a problem when you push a pack update and users still have the
old file in their browser cache. `{% theme_css_link %}` solves this:

```django
{% load djust_theming %}
<head>
    {% theme_css_link %}
</head>
```

It emits a `<link>` with a `?v=<hash>` query string derived from the
active pack's **signature** — the hash of its CSS content, not its version
number. When the pack's CSS changes (either a different pack is activated,
or the same pack is updated in `MEDIA_ROOT`), the hash changes and the
browser fetches the new file.

The `v=` value is the pack's **content hash**, not a timestamp. This means:

- It changes only when the CSS actually changes — not on every deploy.
- It works correctly behind a CDN that ignores `Cache-Control: immutable`
  (some CDNs promote assets to edge on first request and ignore the
  immutable hint until the query string changes).
- Per-user theme preferences get their own correct hash — if user A runs
  `dracula` and user B runs `nord`, each gets the right CDN-cached URL.

### Using it in your own templates

The tag must be inside a `{% load djust_theming %}` block and placed
where a `<link>` is valid (inside `<head>` or at the end of `<body>`).
It takes no arguments — the active pack is read from the request context:

```django
{% load djust_theming %}<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    {% theme_css_link %}
    ...
</head>
```

If you need the raw URL without the `<link>` wrapper (e.g., to inject it
into a custom asset pipeline), use `get_theme_css_url`:

```python
from djust.theming import get_theme_css_url

url = get_theme_css_url(request)   # e.g. "/static/themes/dracula.css?v=3f4a9c2e"
```

## See also

- **[Settings](/theming/settings/)** — the configuration that determines
  defaults and which switchers are available.
- **[Tokens](/theming/tokens/)** — what changes when a switch happens.
- **[Recipes — page-level theme](/theming/recipes/)** — for "different
  look on this one page" without involving the user-facing switcher.
