Theme Toggle
The ThemeToggle component creates a dark/light mode switch with HTMX server-side persistence. It uses Bootstrap's form-switch styling and integrates seamlessly with session-based theme management. Decorative icons are optional.
Goal
By the end of this guide, you'll be able to add a professional dark mode toggle to your app with server-side persistence in minutes.
Quick Start
Here's the simplest way to add a theme toggle.
Visual Examples & Use Cases
1. Toggle With Optional Icon
Minimal toggle without label.
2. With Label
Show label for clarity.
3. In Navbar
Common placement in navigation.
Navbar(
brand="MyApp",
items=[
NavItem("Home", href="/"),
NavItem("About", href="/about"),
# Theme toggle in navbar
ThemeToggle(
current_theme=req.session.get("theme", "light"),
cls="ms-auto"
)
]
)
Practical Functionality
Server-Side Theme Management
Complete theme system with persistence.
# Initialize app with theme support
app = FastHTML()
# Theme toggle in layout
def BaseLayout(*content, req):
theme = req.session.get("theme", "light")
return Html(
Head(
Title("MyApp"),
# Apply theme to body
Script(f"document.documentElement.setAttribute('data-bs-theme', '{theme}')")
),
Body(
Navbar(
brand="MyApp",
items=[
ThemeToggle(
current_theme=theme,
show_label=True
)
]
),
*content,
data_bs_theme=theme
)
)
# Theme toggle endpoint
@app.post("/theme/toggle")
def toggle_theme(req):
current = req.session.get("theme", "light")
new_theme = "dark" if current == "light" else "light"
req.session["theme"] = new_theme
# Refresh page to apply theme
from faststrap.presets import hx_refresh
return hx_refresh()
With Cookie Persistence
Store theme in cookies for non-authenticated users.
@app.post("/theme/toggle")
def toggle_theme(req, res):
current = req.cookies.get("theme", "light")
new_theme = "dark" if current == "light" else "light"
# Set cookie for 1 year
res.set_cookie(
"theme",
new_theme,
max_age=365*24*60*60,
httponly=True
)
return hx_refresh()
With Database Persistence
Save theme preference to user profile.
@app.post("/theme/toggle")
def toggle_theme(req):
user = req.session.get("user")
if not user:
return ErrorDialog(message="Please log in")
# Toggle theme
current = user.theme or "light"
new_theme = "dark" if current == "light" else "light"
# Save to database
db.query(User).filter(User.id == user.id).update({
"theme": new_theme
})
db.commit()
# Update session
req.session["user"].theme = new_theme
return hx_refresh()
Integration Patterns
In Settings Page
def SettingsPage(user):
return Container(
H1("Settings"),
Card(
H5("Appearance", cls="card-title"),
FormGroup(
ThemeToggle(
current_theme=user.theme,
show_label=True,
label_text="Dark Mode"
),
help_text="Toggle between light and dark themes"
),
cls="mb-3"
)
)
With Auto Theme
Support system preference.
def ThemeSelector(current_theme):
return Div(
H6("Theme"),
Div(
# Radio buttons for theme selection
Radio("Light", name="theme", value="light", checked=current_theme=="light"),
Radio("Dark", name="theme", value="dark", checked=current_theme=="dark"),
Radio("Auto (System)", name="theme", value="auto", checked=current_theme=="auto"),
hx_post="/theme/set",
hx_trigger="change"
)
)
@app.post("/theme/set")
def set_theme(theme: str, req):
req.session["theme"] = theme
return hx_refresh()
Smooth Transition
Add CSS for smooth theme transitions.
/* Add to your CSS */
:root {
transition: background-color 0.3s ease, color 0.3s ease;
}
body {
transition: background-color 0.3s ease, color 0.3s ease;
}
Parameter Reference
| Parameter | Type | Default | Description |
|---|---|---|---|
current_theme |
str |
"auto" | Current theme ("light", "dark", "auto") |
endpoint |
str |
"/theme/toggle" | Server endpoint for theme changes |
toggle_id |
str |
"theme-toggle" | Unique ID for the toggle |
show_label |
bool |
False |
Whether to show label text |
label_text |
str |
"Dark Mode" | Label text to display |
show_icon |
bool |
False |
Whether to show the decorative sun/moon icon |
**kwargs |
Any |
- | Additional HTML attributes |
Best Practices
✅ Do This
# Get theme from session
theme = req.session.get("theme", "light")
ThemeToggle(current_theme=theme)
# Provide visual feedback
@app.post("/theme/toggle")
def toggle_theme(req):
# ... toggle logic ...
return hx_refresh() # Refresh to show change
# Support system preference
ThemeToggle(current_theme="auto")
❌ Don't Do This
# Don't hardcode theme
ThemeToggle(current_theme="dark") # Always dark!
# Don't forget to persist
@app.post("/theme/toggle")
def toggle_theme(req):
# Toggle but don't save - lost on refresh!
return hx_refresh()
# Don't use client-side only
# ThemeToggle requires server endpoint
Complete Example
Full theme system implementation.
from fasthtml.common import *
from faststrap import ThemeToggle, Navbar
from faststrap.presets import hx_refresh
app = FastHTML()
def get_theme(req):
"""Get theme from session or cookie."""
return req.session.get("theme") or req.cookies.get("theme", "light")
def apply_theme(theme):
"""Apply theme to HTML."""
return Script(f"""
document.documentElement.setAttribute('data-bs-theme', '{theme}');
""")
@app.get("/")
def home(req):
theme = get_theme(req)
return Html(
Head(
Title("MyApp"),
apply_theme(theme)
),
Body(
Navbar(
brand="MyApp",
items=[
ThemeToggle(
current_theme=theme,
show_label=True
)
]
),
Container(
H1("Welcome to MyApp"),
P("Try toggling the theme!")
),
data_bs_theme=theme
)
)
@app.post("/theme/toggle")
def toggle_theme(req, res):
current = get_theme(req)
new_theme = "dark" if current == "light" else "light"
# Save to session and cookie
req.session["theme"] = new_theme
res.set_cookie("theme", new_theme, max_age=365*24*60*60)
return hx_refresh()
faststrap.components.forms.theme_toggle.ThemeToggle(current_theme='auto', endpoint='/theme/toggle', toggle_id='theme-toggle', show_label=False, label_text='Dark Mode', show_icon=False, **kwargs)
Dark/light mode toggle switch with HTMX persistence.
Creates a toggle switch that POSTs to a server endpoint to persist theme preference. The server should handle the toggle logic and return updated UI or trigger a page refresh.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_theme
|
ThemeType
|
Current theme ("light", "dark", "auto") |
'auto'
|
endpoint
|
str
|
Server endpoint to POST theme changes |
'/theme/toggle'
|
toggle_id
|
str
|
Unique ID for the toggle |
'theme-toggle'
|
show_label
|
bool
|
Whether to show label text |
False
|
label_text
|
str
|
Label text to display |
'Dark Mode'
|
show_icon
|
bool
|
Whether to render the decorative theme icon |
False
|
**kwargs
|
Any
|
Additional HTML attributes |
{}
|
Returns:
| Type | Description |
|---|---|
Div
|
Div containing the theme toggle switch |
Examples:
Basic toggle:
With label:
Custom endpoint:
Server-side handler:
@app.post("/theme/toggle")
def toggle_theme(req: Request):
current = req.session.get("theme", "light")
new_theme = "dark" if current == "light" else "light"
req.session["theme"] = new_theme
# Return updated UI or trigger refresh
from faststrap.presets import hx_refresh
return hx_refresh()
Note
The toggle uses Bootstrap's form-check-input styling. The server endpoint should: 1. Read current theme from session/cookie 2. Toggle to new theme 3. Save to session/cookie 4. Return response (refresh, redirect, or updated HTML)
Source code in src/faststrap/components/forms/theme_toggle.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | |