Creating Custom Themes¶
This guide covers creating custom themes for django-admin-deux.
Theme Architecture¶
Themes are plugins that provide the 'theme' feature. They contribute CSS/JS assets and templates to style the admin interface.
Default theme: django-admin-deux includes a default theme plugin at djadmin/plugins/theme/. This theme is auto-enabled if no alternative theme is installed.
Creating a Theme Plugin¶
Step 1: Create Django App¶
Add to INSTALLED_APPS (order matters - place before default theme to override):
Step 2: Create Plugin Hook¶
# mytheme/djadmin_hooks.py
from djadmin.plugins import hookimpl
from djadmin.actions.base import BaseAction
@hookimpl
def djadmin_provides_features():
"""Advertise theme feature"""
return ['theme']
@hookimpl
def djadmin_get_action_view_assets(action):
"""Provide CSS/JS for all views"""
return {
BaseAction: {
'css': [
'mytheme/css/base.css',
'mytheme/css/components.css',
],
'js': [
'mytheme/js/admin.js',
],
}
}
That's it! Your theme is now active for all views.
Theme Structure¶
Recommended organization:
mytheme/
├── __init__.py
├── djadmin_hooks.py # Plugin registration
├── static/
│ └── mytheme/
│ ├── css/
│ │ ├── base.css # Base styles, layout
│ │ ├── components.css # Buttons, forms, tables
│ │ └── theme.css # Colors, typography
│ └── js/
│ └── admin.js # Interactive features
└── templates/
└── djadmin/ # Template overrides
├── base.html # Base layout
├── actions/
│ ├── add.html
│ ├── edit.html
│ └── list.html
└── includes/
├── header.html
├── sidebar.html
└── footer.html
Asset Management¶
CSS Structure¶
Organize CSS into logical files:
@hookimpl
def djadmin_get_action_view_assets(action):
return {
BaseAction: {
'css': [
'mytheme/css/variables.css', # CSS custom properties
'mytheme/css/base.css', # Layout, grid
'mytheme/css/typography.css', # Fonts, text
'mytheme/css/components.css', # Buttons, cards
'mytheme/css/forms.css', # Form styling
'mytheme/css/tables.css', # Table styling
],
}
}
Action-Specific Assets¶
Target specific action types:
@hookimpl
def djadmin_get_action_view_assets(action):
from djadmin.actions.view_mixins import ListActionMixin
from djadmin.actions.view_mixins import FormViewActionMixin
return {
BaseAction: {
'css': ['mytheme/css/base.css'],
'js': ['mytheme/js/base.js'],
},
ListActionMixin: {
'css': ['mytheme/css/list-view.css'],
'js': ['mytheme/js/list-view.js'],
},
FormViewActionMixin: {
'css': ['mytheme/css/forms.css'],
'js': ['mytheme/js/form-validation.js'],
},
}
External Dependencies¶
Include external libraries (CDN or vendored):
@hookimpl
def djadmin_get_action_view_assets(action):
return {
BaseAction: {
'css': [
'https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css',
'mytheme/css/theme.css', # Your customizations
],
'js': [
'https://unpkg.com/htmx.org@1.9.0',
'mytheme/js/admin.js',
],
}
}
Template Overrides¶
Override default templates to customize markup:
Base Template¶
{# mytheme/templates/djadmin/base.html #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Admin{% endblock %}</title>
{% block css %}
{% for css_file in view.assets.css %}
<link rel="stylesheet" href="{% static css_file %}">
{% endfor %}
{% endblock %}
</head>
<body>
{% include 'djadmin/includes/header.html' %}
<main class="admin-content">
{% block content %}{% endblock %}
</main>
{% include 'djadmin/includes/footer.html' %}
{% block js %}
{% for js_file in view.assets.js %}
<script src="{% static js_file %}"></script>
{% endfor %}
{% endblock %}
</body>
</html>
List View Template¶
{# mytheme/templates/djadmin/actions/list.html #}
{% extends 'djadmin/base.html' %}
{% block content %}
<div class="list-view">
<header class="list-header">
<h1>{{ opts.verbose_name_plural|title }}</h1>
<div class="actions">
{% for action in general_actions %}
<a href="{% url action.url_name %}" class="btn btn-{{ action.css_class }}">
{{ action.label }}
</a>
{% endfor %}
</div>
</header>
<table class="data-table">
{# Your table markup #}
</table>
</div>
{% endblock %}
See default templates: djadmin/plugins/theme/templates/djadmin/
CSS Framework Integration¶
Tailwind CSS Example¶
# mytheme/djadmin_hooks.py
@hookimpl
def djadmin_provides_features():
return ['theme']
@hookimpl
def djadmin_get_action_view_assets(action):
from djadmin.actions.base import BaseAction
return {
BaseAction: {
'css': [
'https://cdn.jsdelivr.net/npm/tailwindcss@3/dist/tailwind.min.css',
'mytheme/css/tailwind-overrides.css',
],
}
}
Template with Tailwind:
{# Use Tailwind classes in templates #}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg p-6">
{# Content #}
</div>
</div>
Bootstrap Example¶
@hookimpl
def djadmin_get_action_view_assets(action):
from djadmin.actions.base import BaseAction
return {
BaseAction: {
'css': [
'https://cdn.jsdelivr.net/npm/bootstrap@5.3/dist/css/bootstrap.min.css',
'mytheme/css/bootstrap-theme.css',
],
'js': [
'https://cdn.jsdelivr.net/npm/bootstrap@5.3/dist/js/bootstrap.bundle.min.js',
],
}
}
Theming Best Practices¶
1. Use CSS Custom Properties¶
Define theme variables for easy customization:
/* mytheme/static/mytheme/css/variables.css */
:root {
/* Colors */
--color-primary: #3b82f6;
--color-secondary: #64748b;
--color-success: #22c55e;
--color-danger: #ef4444;
/* Typography */
--font-family-base: system-ui, sans-serif;
--font-size-base: 16px;
--line-height-base: 1.5;
/* Spacing */
--spacing-unit: 0.25rem;
--border-radius: 0.375rem;
/* Layout */
--sidebar-width: 16rem;
--header-height: 4rem;
}
/* Use variables */
.btn-primary {
background-color: var(--color-primary);
border-radius: var(--border-radius);
}
2. Maintain Consistent Component Structure¶
Keep consistent HTML structure for easier overrides:
{# Consistent button structure #}
<a href="{% url action.url_name %}" class="btn btn-{{ action.css_class }}">
{% if action.icon %}
<span class="icon">{{ action.icon }}</span>
{% endif %}
<span class="label">{{ action.label }}</span>
</a>
3. Progressive Enhancement¶
Start with functional, unstyled HTML, then layer on styles:
/* Base styles (functional without CSS) */
.data-table {
width: 100%;
border-collapse: collapse;
}
/* Enhanced styles */
@media (min-width: 768px) {
.data-table {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
}
Dark Mode Support¶
Implement dark mode using CSS custom properties:
/* variables.css */
:root {
--bg-color: #ffffff;
--text-color: #1f2937;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1f2937;
--text-color: #f9fafb;
}
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}
Or provide a toggle:
// mytheme/static/mytheme/js/theme-toggle.js
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.getElementById('theme-toggle');
toggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark-mode');
localStorage.setItem('theme', document.documentElement.classList.contains('dark-mode') ? 'dark' : 'light');
});
});
Multiple Themes¶
Support theme switching by providing multiple asset bundles:
@hookimpl
def djadmin_get_action_view_assets(action):
from django.conf import settings
theme = getattr(settings, 'DJADMIN_THEME', 'default')
themes = {
'default': {
'css': ['mytheme/css/default.css'],
},
'dark': {
'css': ['mytheme/css/dark.css'],
},
'compact': {
'css': ['mytheme/css/compact.css'],
},
}
return {BaseAction: themes.get(theme, themes['default'])}
Testing Themes¶
Verify theme assets load correctly:
# tests/test_theme.py
import pytest
from django.test import RequestFactory
from djadmin.plugins import pm
def test_theme_provides_feature():
"""Verify theme provides 'theme' feature"""
features = pm.hook.djadmin_provides_features()
assert 'theme' in features
def test_theme_provides_assets(product_factory):
"""Verify theme provides CSS/JS assets"""
from examples.webshop.models import Product
from djadmin import AdminSite, ModelAdmin
site = AdminSite()
admin = ModelAdmin(Product, site)
action = admin.general_actions[0](Product, admin, site)
assets = pm.hook.djadmin_get_action_view_assets(action=action)
# Verify assets present
assert any('css' in asset_dict for asset_list in assets for asset_dict in asset_list)
Reference Implementation¶
Default theme plugin: djadmin/plugins/theme/
Structure:
djadmin/plugins/theme/
├── djadmin_hooks.py # Hook registration
├── static/
│ └── djadmin/
│ └── theme/
│ ├── css/
│ │ └── theme.css
│ └── js/
│ └── admin.js
└── templates/
└── djadmin/
├── base.html
├── actions/
└── includes/
Next Steps¶
- Examples - Theme examples and patterns
- Hook Reference - Asset and template hooks
- Default theme source:
djadmin/plugins/theme/ - Template reference: Django template documentation