Skip to content

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

python manage.py startapp mytheme

Add to INSTALLED_APPS (order matters - place before default theme to override):

INSTALLED_APPS = [
    'djadmin',
    'mytheme',  # Your custom theme
    # ...
]

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