Skip to content
djust/docs
Appearance
Mode
djust.org →
Browse documentation

4 min read

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)

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.

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

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

Spotted a typo or want to improve this page? Edit on GitHub →