Dark Mode and Theming¶
This guide explains how dark mode works in django-admin-deux and how to customize themes.
Overview¶
django-admin-deux includes built-in dark mode support with automatic system preference detection and manual toggle capability. The dark mode is provided by the default theme plugin and persists user preferences using localStorage.
How Dark Mode Works¶
Automatic Detection¶
Dark mode automatically detects your system preference:
// Checks system preference
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
Priority Order:
1. User's saved preference in localStorage (highest priority)
2. System preference (prefers-color-scheme: dark)
3. Light mode (default fallback)
Manual Toggle¶
A toggle button appears in the top-right header:
┌────────────────────────────────┐
│ Django Admin [User] [🌙] │
│ ↑ │
│ Dark mode │
│ toggle │
└────────────────────────────────┘
Clicking the toggle: - Switches between light and dark modes - Saves preference to localStorage - Preference persists across sessions
localStorage Persistence¶
Your preference is stored in localStorage:
// Saved values
localStorage.setItem('theme', 'dark'); // Dark mode
localStorage.setItem('theme', 'light'); // Light mode
// Retrieve saved theme
const savedTheme = localStorage.getItem('theme');
System Preference Changes¶
Dark mode listens for system preference changes:
// Auto-updates when system preference changes (if no saved preference)
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches);
}
});
Note: System preference only applies if the user hasn't manually set a preference.
Using Dark Mode¶
Enable Dark Mode¶
Dark mode is enabled by default. Users can:
- Use system preference: Set dark mode in their OS, and django-admin-deux will follow
- Manual toggle: Click the toggle button in the header
- Set preference: Choice is saved and persists across sessions
Clear Saved Preference¶
To revert to system preference:
Then refresh the page. Dark mode will now follow system preference.
Customizing Dark Mode¶
Dark Mode Colors¶
The theme uses Tailwind CSS with custom color variables. Colors are defined in the CSS with .dark class variants:
/* Light mode */
body {
background-color: rgb(243 244 246);
color: rgb(17 24 39);
}
/* Dark mode */
.dark body {
background-color: rgb(3 7 18);
color: rgb(243 244 246);
}
Override Dark Mode Colors¶
Create a custom CSS file to override colors:
/* static/css/admin-dark-overrides.css */
/* Custom dark mode background */
.dark body {
background-color: #1a1a2e;
}
/* Custom dark mode card background */
.dark .admin article {
background-color: #16213e;
border-color: #0f3460;
}
/* Custom dark mode primary button */
.dark .admin a.primary,
.dark .admin button.primary {
background-color: #e94560;
}
.dark .admin a.primary:hover,
.dark .admin button.primary:hover {
background-color: #d63649;
}
Include it in your template:
{# templates/djadmin/admin_base.html #}
{% extends "djadmin/admin_base.html" %}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'css/admin-dark-overrides.css' %}">
{% endblock %}
Color Scheme Reference¶
Default Dark Mode Palette:
| Element | Light Mode | Dark Mode |
|---|---|---|
| Body Background | #f3f4f6 |
#030712 |
| Card Background | #ffffff |
#111827 |
| Card Border | #d1d5db |
#1f2937 |
| Text Primary | #111827 |
#f3f4f6 |
| Text Secondary | #6b7280 |
#9ca3af |
| Primary Button | #0c4b33 |
#0a3d2a |
| Primary Hover | #0a3d2a |
#082f21 |
| Danger Button | #dc2626 |
#b91c1c |
Custom Dark Mode Detection¶
Override the dark mode detection logic:
// static/js/custom-dark-mode.js
// Disable default dark mode
localStorage.setItem('theme', 'light');
// Implement custom logic
function customDarkModeCheck() {
const hour = new Date().getHours();
// Dark mode between 8 PM and 6 AM
return hour >= 20 || hour < 6;
}
if (customDarkModeCheck()) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
Disabling Dark Mode¶
Method 1: Hide Toggle Button¶
Hide the toggle button to prevent manual switching:
Users will follow system preference only.
Method 2: Force Light Mode¶
Always use light mode:
// static/js/force-light-mode.js
document.addEventListener('DOMContentLoaded', function() {
// Remove dark class
document.documentElement.classList.remove('dark');
// Clear saved preference
localStorage.removeItem('theme');
// Disable toggle
const toggle = document.getElementById('dark-mode-toggle');
if (toggle) {
toggle.style.display = 'none';
}
});
Include in your template:
{% block extra_js %}
{{ block.super }}
<script src="{% static 'js/force-light-mode.js' %}"></script>
{% endblock %}
Method 3: Force Dark Mode¶
Always use dark mode:
// static/js/force-dark-mode.js
document.addEventListener('DOMContentLoaded', function() {
// Add dark class
document.documentElement.classList.add('dark');
// Save preference
localStorage.setItem('theme', 'dark');
// Hide toggle
const toggle = document.getElementById('dark-mode-toggle');
if (toggle) {
toggle.style.display = 'none';
}
});
Alternative Themes¶
Creating a Custom Theme Plugin¶
django-admin-deux uses a plugin system for themes. You can create a custom theme plugin:
1. Create Plugin Structure:
myapp/
├── djadmin_hooks.py
├── static/
│ └── myapp/
│ └── theme.css
└── templates/
└── djadmin/
├── admin_base.html
└── includes/
└── styles.html
2. Implement Hook (djadmin_hooks.py):
from djp import hookimpl
@hookimpl
def djadmin_provides_features():
"""Advertise that this plugin provides a theme"""
return ['theme']
@hookimpl
def djadmin_get_theme_assets():
"""Provide custom theme CSS/JS"""
return {
'css': ['myapp/theme.css'],
'js': ['myapp/theme.js'],
}
3. Create Base Template (templates/djadmin/admin_base.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title>
<!-- Your custom theme CSS -->
<link rel="stylesheet" href="{% static 'myapp/theme.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body>
<div class="admin">
<header>
{% block header %}
<!-- Your custom header -->
{% endblock %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
{% block footer %}{% endblock %}
</footer>
</div>
<script src="{% static 'myapp/theme.js' %}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
4. Install Plugin:
# settings.py
INSTALLED_APPS = [
'djadmin',
'djadmin.plugins.core',
# 'djadmin.plugins.theme', # Remove default theme
'myapp', # Your custom theme plugin
# ... other apps
]
Theme Plugin Examples¶
Minimal Theme (no dark mode):
# myapp/djadmin_hooks.py
from djp import hookimpl
@hookimpl
def djadmin_provides_features():
return ['theme']
@hookimpl
def djadmin_get_theme_assets():
return {
'css': ['myapp/minimal-theme.css'],
}
Bootstrap Theme:
@hookimpl
def djadmin_get_theme_assets():
return {
'css': [
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css',
'myapp/bootstrap-overrides.css',
],
'js': [
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js',
],
}
Material Design Theme:
@hookimpl
def djadmin_get_theme_assets():
return {
'css': [
'https://fonts.googleapis.com/icon?family=Material+Icons',
'myapp/material-theme.css',
],
'js': ['myapp/material-components.js'],
}
Tailwind CSS Customization¶
The default theme uses Tailwind CSS. To customize the Tailwind configuration:
1. Create tailwind.config.js:
// tailwind.config.js
module.exports = {
content: [
'./templates/**/*.html',
'./static/**/*.js',
],
darkMode: 'class', // Enable class-based dark mode
theme: {
extend: {
colors: {
// Custom brand colors
primary: {
50: '#f0fdf4',
// ... your color scale
900: '#052e16',
},
},
},
},
plugins: [
require('@tailwindcss/forms'),
],
};
2. Build Custom CSS:
# Install Tailwind
npm install -D tailwindcss @tailwindcss/forms
# Build CSS
npx tailwindcss -i ./src/input.css -o ./static/css/theme.css --watch
3. Use Custom CSS:
Testing Dark Mode¶
Visual Testing¶
Test both modes manually:
- Toggle dark mode in the admin
- Check all pages (dashboard, list, create, update, delete)
- Verify colors have sufficient contrast
- Test with different screen sizes
Automated Testing¶
Test dark mode detection:
from django.test import TestCase, Client
class DarkModeTest(TestCase):
def test_dark_mode_toggle_exists(self):
"""Test that dark mode toggle is present"""
client = Client()
response = client.get('/djadmin/')
self.assertContains(response, 'id="dark-mode-toggle"')
def test_dark_mode_javascript_loaded(self):
"""Test that dark mode JavaScript is loaded"""
response = self.client.get('/djadmin/')
self.assertContains(response, 'initDarkMode')
Accessibility Testing¶
Verify color contrast in both modes:
# Use tools like:
# - WAVE Browser Extension
# - axe DevTools
# - Chrome Lighthouse
# Test contrast ratios:
# - Normal text: at least 4.5:1
# - Large text: at least 3:1
# - UI components: at least 3:1
Best Practices¶
1. Maintain Contrast Ratios¶
Ensure readable text in both modes:
/* Good contrast */
.dark body {
background: #1a1a1a; /* Very dark */
color: #f5f5f5; /* Very light */
}
/* Poor contrast (avoid) */
.dark body {
background: #555555; /* Medium gray */
color: #888888; /* Medium gray */
}
2. Test Both Modes¶
Always test your customizations in both light and dark mode.
3. Respect User Preferences¶
Don't override user preferences without good reason:
// Good: Respect saved preference
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
applyTheme(savedTheme === 'dark');
}
// Bad: Always override
applyTheme(true); // Forces dark mode
4. Provide Clear Toggle¶
Make the dark mode toggle obvious and accessible:
<button id="dark-mode-toggle" aria-label="Toggle dark mode">
<span class="sun" aria-hidden="true">☀️</span>
<span class="moon" aria-hidden="true">🌙</span>
</button>
5. Use Semantic Colors¶
Use semantic color names that work in both modes:
/* Good: Semantic */
.success { color: var(--color-success); }
.error { color: var(--color-error); }
/* Bad: Hard-coded */
.success { color: #00ff00; } /* Too bright in dark mode */
Troubleshooting¶
Dark Mode Not Working¶
Problem: Dark mode toggle doesn't work
Solutions:
1. Check that JavaScript is loaded: View page source, look for admin.js
2. Check browser console for errors
3. Verify localStorage is not disabled
4. Clear localStorage and try again: localStorage.clear()
Dark Mode Flashing¶
Problem: Page flashes light mode before switching to dark
Solution: Add inline script in <head>:
<script>
// Apply theme before page renders
(function() {
const savedTheme = localStorage.getItem('theme');
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = savedTheme === 'dark' || (!savedTheme && systemPrefersDark);
if (isDark) {
document.documentElement.classList.add('dark');
}
})();
</script>
Colors Not Updating¶
Problem: Custom colors not appearing in dark mode
Solution: Ensure you're using .dark prefix:
/* Wrong */
body {
background: #1a1a1a; /* Applies to light mode too */
}
/* Correct */
.dark body {
background: #1a1a1a; /* Only applies in dark mode */
}
Toggle Not Visible¶
Problem: Can't find dark mode toggle
Solution: Toggle is in the header navigation:
{# templates/djadmin/admin_base.html #}
<nav>
<a href="{% url 'djadmin:index' %}">Django Admin</a>
<!-- Other nav items -->
<button id="dark-mode-toggle">🌙</button>
</nav>
See Also¶
- Customization Guide - Advanced customization techniques
- Plugin Development - Creating Themes - Build custom theme plugins
- Theming Documentation - Complete theming reference
- Installation - Setting up django-admin-deux