Back to Blog

Implementing Dark Mode in Django with JavaScript and CSS

admin
November 30, 2025 4 min read
13 views
Build a beautiful dark mode toggle that persists user preferences using localStorage and CSS custom properties.

# Implementing Dark Mode in Django

Dark mode isn't just a trend—it's an accessibility feature that reduces eye strain. Here's how we implemented it in DjangoZen.

## The Strategy

1. Use CSS custom properties (variables) for colors
2. Toggle a class on the body element
3. Persist preference in localStorage
4. Respect system preferences as default

## CSS Custom Properties

Define your color palette:

```css
/* Light mode (default) */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-primary: #212529;
--text-secondary: #6c757d;
--text-muted: #adb5bd;
--border-color: #dee2e6;
--card-bg: #ffffff;
--navbar-bg: linear-gradient(135deg, #2d3561 0%, #3d2656 100%);
--shadow: rgba(0, 0, 0, 0.1);
--accent: #667eea;
}

/* Dark mode */
body.dark-mode {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-tertiary: #0f3460;
--text-primary: #e8e8e8;
--text-secondary: #b8b8b8;
--text-muted: #888888;
--border-color: #2d2d44;
--card-bg: #1e1e32;
--navbar-bg: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
--shadow: rgba(0, 0, 0, 0.3);
--accent: #7c8ef5;
}
```

## Apply Variables to Elements

```css
body {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}

.card {
background-color: var(--card-bg);
border-color: var(--border-color);
}

.navbar {
background: var(--navbar-bg);
}

.text-muted {
color: var(--text-muted) !important;
}

.form-control {
background-color: var(--bg-secondary);
border-color: var(--border-color);
color: var(--text-primary);
}

.form-control:focus {
background-color: var(--bg-secondary);
color: var(--text-primary);
border-color: var(--accent);
}

/* Smooth transitions */
.card, .form-control, .btn, .navbar {
transition: background-color 0.3s ease,
border-color 0.3s ease,
color 0.3s ease;
}
```

## The Toggle Button

Add a toggle in your navbar:

```html
<!-- base.html -->
<button id="darkModeToggle"
class="btn btn-link nav-link px-2"
title="Toggle Dark Mode">
<i class="fas fa-moon" id="darkModeIcon"></i>
</button>
```

Style the toggle:

```css
#darkModeToggle {
cursor: pointer;
transition: transform 0.3s ease;
border: none;
background: none;
}

#darkModeToggle:hover {
transform: scale(1.1);
}

#darkModeToggle:hover i {
color: var(--accent);
}
```

## JavaScript Implementation

```javascript
document.addEventListener('DOMContentLoaded', function() {
const darkModeToggle = document.getElementById('darkModeToggle');
const darkModeIcon = document.getElementById('darkModeIcon');
const body = document.body;

// Check for saved preference or system preference
function getPreferredTheme() {
const savedTheme = localStorage.getItem('darkMode');
if (savedTheme !== null) {
return savedTheme === 'enabled';
}
// Check system preference
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}

// Apply theme
function applyTheme(isDark) {
if (isDark) {
body.classList.add('dark-mode');
darkModeIcon.classList.remove('fa-moon');
darkModeIcon.classList.add('fa-sun');
} else {
body.classList.remove('dark-mode');
darkModeIcon.classList.remove('fa-sun');
darkModeIcon.classList.add('fa-moon');
}
}

// Initialize on page load
applyTheme(getPreferredTheme());

// Toggle handler
darkModeToggle.addEventListener('click', function() {
const isDark = body.classList.toggle('dark-mode');

// Update icon
if (isDark) {
darkModeIcon.classList.remove('fa-moon');
darkModeIcon.classList.add('fa-sun');
localStorage.setItem('darkMode', 'enabled');
} else {
darkModeIcon.classList.remove('fa-sun');
darkModeIcon.classList.add('fa-moon');
localStorage.setItem('darkMode', 'disabled');
}
});

// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (localStorage.getItem('darkMode') === null) {
applyTheme(e.matches);
}
});
});
```

## Handling Images

Some images need different versions for dark mode:

```html
<picture>
<source srcset="/static/img/logo-dark.png" media="(prefers-color-scheme: dark)">
<img src="/static/img/logo-light.png" alt="Logo" class="theme-aware-image">
</picture>
```

Or with CSS:

```css
.logo-light {
display: block;
}
.logo-dark {
display: none;
}

body.dark-mode .logo-light {
display: none;
}
body.dark-mode .logo-dark {
display: block;
}
```

## Preventing Flash of Wrong Theme

Add this in the `<head>` before CSS loads:

```html
<script>
// Apply theme immediately to prevent flash
(function() {
const savedTheme = localStorage.getItem('darkMode');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

if (savedTheme === 'enabled' || (savedTheme === null && prefersDark)) {
document.documentElement.classList.add('dark-mode');
}
})();
</script>
```

And add CSS for `html.dark-mode`:

```css
html.dark-mode body {
background-color: #1a1a2e;
color: #e8e8e8;
}
```

## Component-Specific Styles

### Dropdown Menus

```css
body.dark-mode .dropdown-menu {
background-color: #2d2d44;
border-color: #3d3d5c;
}

body.dark-mode .dropdown-item {
color: #e0e0e0;
}

body.dark-mode .dropdown-item:hover {
background-color: #3d3d5c;
}

body.dark-mode .dropdown-divider {
border-color: #3d3d5c;
}
```

### Tables

```css
body.dark-mode .table {
color: var(--text-primary);
}

body.dark-mode .table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(255, 255, 255, 0.02);
}

body.dark-mode .table-hover tbody tr:hover {
background-color: rgba(255, 255, 255, 0.05);
}
```

### Modals

```css
body.dark-mode .modal-content {
background-color: var(--card-bg);
border-color: var(--border-color);
}

body.dark-mode .modal-header,
body.dark-mode .modal-footer {
border-color: var(--border-color);
}

body.dark-mode .btn-close {
filter: invert(1);
}
```

## User Preference in Database

For logged-in users, persist to database:

```python
# models.py
class UserPreferences(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
dark_mode = models.BooleanField(default=False)

# views.py
@login_required
def toggle_dark_mode(request):
prefs, _ = UserPreferences.objects.get_or_create(user=request.user)
prefs.dark_mode = not prefs.dark_mode
prefs.save()
return JsonResponse({'dark_mode': prefs.dark_mode})
```

## Accessibility Considerations

1. **Contrast ratios**: Ensure WCAG 2.1 AA compliance (4.5:1 for text)
2. **Focus indicators**: Make them visible in both modes
3. **Reduced motion**: Respect `prefers-reduced-motion`

```css
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
}
}
```

## Testing

Test in multiple browsers and check:
- Toggle works correctly
- Preference persists on refresh
- System preference fallback works
- No flash of wrong theme
- All components styled correctly

Dark mode makes your app more accessible and user-friendly!

Comments (0)

Please login to leave a comment.

No comments yet. Be the first to comment!