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):
{% 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):
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
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:
Active pack: {{ request.theme.pack.display_name }}
Mode: {{ request.theme.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):
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:
# 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.
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:
{% 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
draculaand user B runsnord, 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:
{% 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:
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 — the configuration that determines defaults and which switchers are available.
- Tokens — what changes when a switch happens.
- Recipes — page-level theme — for "different look on this one page" without involving the user-facing switcher.