v2.0.0 to v2.0.0

129 added, 276 removed. Audit A to A.

---
name: django-unfold
description: Use when theming the Django admin with django-unfold - settings and sidebar configuration, custom admin site, components and @display decorator, filters, actions, tabs, dark mode and styling, or Django 6.x compatibility
metadata:
author: mte90
version: 2.0.0
based_on: https://github.com/unfoldadmin/django-unfold
tags:
- python
- django
- admin
- unfold
- theme
- dashboard
---
# Django Unfold
- Modern Django admin theme with beautiful design and advanced features.
+ > **Objective:** Use this skill to reduce admin-broken-layout incidents, @display-misuse bugs, filter-registration failures, and dark-mode regressions by enforcing settings-discipline, decorator-contract rules, and component-registration patterns below.
- ## Overview
+ Modern Django admin theme with Tailwind-based design.
- Unfold is a modern theme for Django admin that provides:
+ ---
- **Versions**: django-unfold 0.76.x + Django 6.0 fully compatible.
- - **Beautiful design** - Modern UI with Tailwind CSS
- - **Dark mode** - Built-in dark theme support
- - **Custom components** - Charts, tables, cards, buttons
- - **Easy customization** - Settings, branding, sidebar
- - **Integrations** - Works with django-celery-beat, django-import-export, etc.
+ ## Core Rules (do X because Y)
+ 1. **Put `unfold` before `django.contrib.admin` in INSTALLED_APPS** because the template engine loads the first admin app it finds and later apps are ignored.
+
+ 2. **Extend `unfold/base.html` not `admin/base.html`** because extending the stock Django base bypasses Unfold's CSS, sidebar, and dark-mode blocks.
+
+ 3. **Use UPPER_CASE keys in UNFOLD dict** because lowercase keys are silently ignored at runtime.
+
+ 4. **Define UNFOLD in settings.py not admin.py** because the theme reads settings at import time; putting it in admin.py causes partial initialization.
+
+ 5. **Register @display methods before list_display references them** because late decorators don't bind to the admin metaclass and the column renders empty.
+
+ 6. **Use UNFOLD's COLORS palette not raw CSS overrides** because !important rules fight the Tailwind cascade and break on Unfold updates.
+
+ 7. **Test dark mode with system preference toggled** because DARK_MODE_COLORS that mirror light mode create invisible text and broken contrast.
+
---
## Installation
```bash
pip install django-unfold
-
- # Or with specific version
- pip install django-unfold==0.9.0
```
```python
# settings.py
INSTALLED_APPS = [
'unfold', # Must come BEFORE django.contrib.admin
'django.contrib.admin',
'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- # ... other apps
+ # ...
]
- # Unfold settings
UNFOLD = {
- # Configuration here
+ 'SITE_HEADER': 'My Admin',
}
```
- > **Important**: `unfold` must be first in `INSTALLED_APPS` to override Django templates.
-
---
- ## Quick Start
+ ## Custom Admin Site
- ### Basic Configuration
+ **Do this because**: Default admin site class doesn't expose custom context or override authentication flow.
```python
- # settings.py
- INSTALLED_APPS = [
- 'unfold',
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- ]
-
- # Optional: Add your custom admin site class
- # UNFOLD_ADMIN_SITE_CLASS = 'path.to.CustomAdminSite'
- ```
-
- ### URL Configuration
-
- Unfold doesn't require changes to your URL configuration:
+ # admin.py
+ from unfold.admin import UnfoldAdminSite
- ```python
- # urls.py - No changes needed!
- from django.urls import path, include
+ class CustomAdminSite(UnfoldAdminSite):
+ site_header = 'Custom Portal'
+
+ def each_context(self, request):
+ context = super().each_context(request)
+ context['custom_branding'] = get_branding()
+ return context
- urlpatterns = [
- path('admin/', admin.site.urls),
- # ... other urls
- ]
+ admin_site = CustomAdminSite(name='custom')
```
- ---
-
- ## Integrations
-
- ### django-guardian (Object Permissions)
-
```python
- # Install
- pip install django-guardian
-
- # Add to INSTALLED_APPS
- INSTALLED_APPS = [
- 'guardian',
- 'unfold',
- 'django.contrib.admin',
- # ...
- ]
-
- # Add to URLs
- from django.contrib import admin
- from unfold.contrib.guardian.admin import GuardedModelAdmin
-
- class ArticleAdmin(GuardedModelAdmin):
- pass
+ # settings.py
+ UNFOLD_ADMIN_SITE_CLASS = 'myapp.admin.CustomAdminSite'
```
- ### django-import-export
-
- ```python
- pip install django-import-export
-
- # Works automatically with Unfold
- # Custom resource if needed
- from import_export import resources
- from unfold.admin import ImportExportMixin
+ See `references/settings-customization.md` for full settings reference, sidebar config, and dark mode controls.
- class ArticleResource(resources.ModelResource):
- class Meta:
- model = Article
- fields = ('id', 'title', 'status', 'created_at')
+ ---
- class ArticleAdmin(ImportExportMixin, ModelAdmin):
- resource_classes = [ArticleResource]
- ```
+ ## @display Decorator (Django 6.0+)
- ### django-celery-beat
+ **Do this because**: Without proper decorator usage, list_display columns render empty or unsortable.
```python
- # Install
- pip install django-celery-beat
+ from unfold.decorators import display
- # Configuration is automatic with Unfold
- # Shows schedule in admin dashboard
+ class UserAdmin(ModelAdmin):
+ @display(description="Active", boolean=True)
+ def is_active(self, obj):
+ return obj.is_active
+
+ @display(description="Full Name", order="username")
+ def full_name(self, obj):
+ return f"{obj.first_name} {obj.last_name}"
+
+ list_display = ['username', 'is_active', 'full_name']
```
- ### django-constance
-
- ```python
- pip install django-constance[database]
-
- INSTALLED_APPS = [
- 'constance',
- 'unfold',
- # ...
- ]
+ **Failure mode**: Column shows blank because `@display` must be applied before the method is referenced in `list_display`.
- # Configuration is automatic
- # Shows in admin under "Constance" section
- ```
+ See `references/components-actions.md` for @display parameters and field/widget patterns.
---
- ## Authentication Customization
-
- ### Custom Login Form with Design System Integration
+ ## Filters
- **When you need this**: Customize the admin login page to match your design system using unfold's template overrides.
+ **Do this because**: Default admin filters don't integrate with Unfold's dropdown styling and can break on large FK fields.
```python
- # settings.py - Override login template
- def get_custom_login_template(request):
- return "admin/custom_login.html"
+ import unfold.filters as filters
- UNFOLD = {
- 'TEMPLATES': {
- 'login': get_custom_login_template,
- },
- 'LOGIN': {
- 'show_language_switcher': False,
- 'extra_fields': [
- {'field': 'organization', 'type': 'select', 'required': True},
- ],
- },
- }
+ class ArticleAdmin(ModelAdmin):
+ list_filter = [
+ ('status', filters.DropdownFilter), # Better than SelectFilter for long lists
+ ('author', filters.AutocompleteFilter), # Required for >100 authors
+ ('created_at', filters.DateRangeFilter),
+ ]
```
- ```html
- <!-- templates/admin/custom_login.html -->
- {% extends "unfold/login.html" %}
- {% load i18n %}
+ **Failure mode**: Filter dropdown renders as stock Django select because you used `list_filter = ['field']` without Unfold's filter class.
- {% block branding %}
- <div class="flex items-center gap-3 mb-6">
- <img src="/static/img/company-logo.svg" alt="Company" class="h-10">
- <span class="text-xl font-semibold">Admin Portal</span>
- </div>
- {% endblock %}
- ```
+ See `references/components-actions.md` for complete filter type listings.
- ### Custom UserAdmin with Additional Fields
+ ---
- **When you need this**: Add custom fields to the user admin form for organization-specific user attributes.
+ ## Actions
- ```python
- # admin.py
- from django.contrib import admin
- from unfold.admin import ModelAdmin
- from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
- from django.contrib.auth.models import User
- from unfold.contrib.filters import AutocompleteFilter
+ **Do this because**: Stock admin actions lack Unfold's button styling and confirmation UI.
- class OrganizationFilter(AutocompleteFilter):
- title = "Organization"
- field_name = "organization"
+ ```python
+ from unfold.decorators import action
- @admin.register(User)
- class UserAdmin(BaseUserAdmin):
- # Inherit Unfold styling while adding custom fields
- fieldsets = BaseUserAdmin.fieldsets + (
- ("Organization", {"fields": ("organization", "employee_id")}),
- )
-
- add_fieldsets = BaseUserAdmin.add_fieldsets + (
- ("Organization", {"fields": ("organization",)}),
- )
+ class ArticleAdmin(ModelAdmin):
+ @action(description='Publish selected articles')
+ def publish_selected(self, request, queryset):
+ queryset.update(status='published')
- list_filter = BaseUserAdmin.list_filter + ("organization", OrganizationFilter)
+ actions = ['publish_selected']
```
- ### Custom Admin Site Authentication
-
- **When you need this**: Override the entire authentication flow with custom admin site class.
-
- ```python
- # admin.py
- from unfold.admin import UnfoldAdminSite
- from django.contrib.auth.forms import AuthenticationForm
-
- class CustomAuthForm(AuthenticationForm):
- def confirm_login_allowed(self, user):
- # Custom authentication logic
- if not user.is_active:
- raise ValidationError("Account inactive")
- if user.organization.status != "active":
- raise ValidationError("Organization suspended")
-
- class CustomAdminSite(UnfoldAdminSite):
- def login(self, request, extra_context=None):
- extra_context = extra_context or {}
- extra_context["custom_message"] = "Welcome to the admin"
- return super().login(request, extra_context)
-
- admin_site = CustomAdminSite(name="custom")
- ```
+ **Failure mode**: Action runs but shows no success message because `message_level` wasn't set or the method wasn't registered in `actions`.
- ```python
- # settings.py
- UNFOLD_ADMIN_SITE_CLASS = "myapp.admin.CustomAdminSite"
- ```
+ See `references/components-actions.md` for row actions and CSV export patterns.
---
## Common Pitfalls
### INSTALLED_APPS Ordering
**Failure**: Unfold silently ignored, admin renders with default Django theme.
```python
- # WRONG - unfold must come BEFORE django.contrib.admin
+ # WRONG
INSTALLED_APPS = [
- 'django.contrib.admin', # ❌ Too late, templates already loaded
+ 'django.contrib.admin', # ❌ Already loaded templates
'unfold',
]
# CORRECT
INSTALLED_APPS = [
- 'unfold', # ✅ Template engine override works
+ 'unfold', # ✅ Template override works
'django.contrib.admin',
]
```
- ---
-
### Template Extension Mistake
- **Failure**: Custom template doesn't apply Unfold styling because it extends wrong base.
-
- ```python
- # WRONG - extends stock Django admin, bypasses Unfold
- {% extends "admin/base.html" %} # ❌ Missing Unfold blocks
-
- # CORRECT - extends Unfold's base template
- {% extends "unfold/base.html" %} # ✅ Gets Unfold's CSS, layout, sidebar
- ```
-
- ### UNFOLD Settings Case Sensitivity
-
- **Failure**: Settings silently ignored due to lowercase keys.
+ **Failure**: Custom template loses Unfold styling.
```python
- # WRONG - keys must be UPPER_CASE
- UNFOLD = {
- 'site_header': 'My Admin', # ❌ Ignored
- 'show_sidebar': True, # ❌ Ignored
- }
+ # WRONG
+ {% extends "admin/base.html" %} # ❌ No Unfold blocks
# CORRECT
- UNFOLD = {
- 'SITE_HEADER': 'My Admin', # ✅
- 'SHOW_SIDEBAR': True, # ✅
- }
+ {% extends "unfold/base.html" %} # ✅ Gets CSS, layout, sidebar
```
### Styling Without Theme Pipeline
- **Failure**: Custom CSS conflicts with Unfold's Tailwind classes, requires constant maintenance.
+ **Failure**: Custom CSS conflicts with Unfold's Tailwind classes.
```python
- # WRONG - raw CSS file overrides Unfold classes
+ # WRONG
UNFOLD = {
- 'STYLES': ['css/custom.css'], # ❌ Brittle, breaks on Unfold updates
+ 'STYLES': ['css/custom.css'],
}
-
/* custom.css */
- .sidebar { background: blue !important; } /* ❌ Fighting framework */
+ .sidebar { background: blue !important; } # ❌ Brittle
```
- **Correct approach**: Use Unfold's theme customization or Tailwind config.
+ **Correct**:
```python
- # CORRECT - extend Unfold's color system
UNFOLD = {
'COLORS': {
'primary': {
- '50': '239 246 255',
'500': '59 130 246',
- # ... full palette
},
},
}
```
---
- ## Integrations Decision Guide
+ ## Integrations
- ### django-guardian (Object Permissions)
+ ### django-guardian
- **Solves**: Per-object permissions beyond Django's model-level permissions.
+ **Do this because**: You need per-object permissions beyond Django's model-level permissions.
- - **Choose it when**: You need granular access control (e.g., users can only edit their own records)
- - **Skip it when**: Model-level permissions (`add`, `change`, `delete`, `view`) are sufficient
+ ```python
+ INSTALLED_APPS = ['guardian', 'unfold', 'django.contrib.admin']
+ from unfold.contrib.guardian.admin import GuardedModelAdmin
+
+ class ArticleAdmin(GuardedModelAdmin):
+ pass
+ ```
+
**URL**: https://github.com/django-guardian/django-guardian
### django-import-export
- **Solves**: Import/export data via CSV, Excel, YAML in admin.
+ **Do this because**: Non-technical users need bulk import/export via admin.
- - **Choose it when**: Non-technical users need to bulk-import or export data through admin
- - **Skip it when**: Data migration is one-time or handled by API/ETL tools
+ ```python
+ from import_export import resources
+ from unfold.admin import ImportExportMixin
+ class ArticleResource(resources.ModelResource):
+ class Meta:
+ model = Article
+ fields = ('id', 'title', 'status')
+
+ class ArticleAdmin(ImportExportMixin, ModelAdmin):
+ resource_classes = [ArticleResource]
+ ```
+
**URL**: https://github.com/django-import-export/django-import-export
### django-celery-beat
- **Solves**: Schedule periodic Celery tasks via admin interface.
+ **Do this because**: You need cron-like scheduling with admin UI.
- - **Choose it when**: You need cron-like scheduling with admin UI for task management
- - **Skip it when**: Tasks run on fixed intervals (use Celery beat config) or triggered by events
+ ```python
+ # Automatic integration - no config needed
+ pip install django-celery-beat
+ ```
**URL**: https://github.com/celery/django-celery-beat
### django-constance
- **Solves**: Runtime site-wide settings editable via admin.
+ **Do this because**: You need runtime site-wide settings without redeploy.
- - **Choose it when**: You need feature flags or configuration values changed without redeploy
- - **Skip it when**: Settings are static or managed via environment variables
+ ```python
+ INSTALLED_APPS = ['constance', 'unfold']
+ # Automatic integration
+ ```
**URL**: https://github.com/jazzband/django-constance
- ### django-unfold-modal
-
- **Solves**: Replace admin popup windows with in-page modals for related object selection.
-
- - **Choose it when**: UX polish matters and you want seamless related-object creation
- - **Skip it when**: Popups are acceptable or you're using autocomplete_fields exclusively
-
- **URL**: https://github.com/metaforx/django-unfold-modal
-
- ### django-unfold-markdown
-
- **Solves**: Markdown editor widget for admin text fields.
-
- - **Choose it when**: Content editors need Markdown formatting without rich-text overhead
- - **Skip it when**: You need WYSIWYG editing or plain text is sufficient
-
- **URL**: https://github.com/sergei-vasilev-dev/django-unfold-markdown
+ See `references/ecosystem-compat.md` for django-unfold-modal, django-unfold-markdown, and Django 6.x compatibility patches.
---
## When Not to Use Unfold
- Plain Django admin is sufficient when:
+ Stock Django admin is sufficient when:
- - **Small internal projects** - Admin used by <5 power users, no branding requirements
- - **Heavy customization needed** - Unfold's template override layer becomes maintenance cost when you're rewriting 50% of templates
- - **No Tailwind expertise** - Adding Tailwind build pipeline just for admin theming is overkill
- - **Budget constraints** - Unfold is free, but Tailwind compilation and theme maintenance have hidden costs
+ - **Small internal projects** — Admin used by <5 power users, no branding requirements
+ - **Heavy customization needed** — Unfold's template override layer becomes maintenance cost when rewriting 50% of templates
+ - **No Tailwind expertise** — Adding Tailwind build pipeline just for admin theming is overkill
**Check before adopting**:
1. Does your team have Tailwind CSS experience?
2. Will admin branding be reviewed by stakeholders?
- 3. Are you comfortable maintaining template overrides if Unfold breaks them?
- 4. Is the admin used frequently enough to justify the UX investment?
-
- If answers are "no" to most, stick with stock Django admin or consider lighter themes.
-
- ---
-
- ## Django Version Compatibility
-
- | Unfold Version | Django Version |
- |----------------|----------------|
- | 0.76.x + | 6.0+ |
- | 0.70.x - 0.75.x | 5.0+, 5.1+ |
- | 0.60.x - 0.69.x | 4.2+, 5.0+ |
- | 0.50.x - 0.59.x | 4.1+, 4.2+ |
-
- **Full compatibility matrix**: https://unfoldadmin.com/docs/installation/#compatibility
+ 3. Is the admin used frequently enough to justify the UX investment?
---
## Deep Dives
For detailed reference, load these on-demand:
- - **Settings & Customization** — `references/settings-customization.md` - Settings reference, custom admin site, dark mode, custom styling
- - **Components & Actions** — `references/components-actions.md` - Components, fields, filters, actions, tabs
- - **Ecosystem & Compatibility** — `references/ecosystem-compat.md` - Ecosystem libraries, Django 6.x compatibility, ordering notes
+ - **Settings & Customization** — `references/settings-customization.md` - Full settings reference, custom admin site, dark mode, sidebar config
+ - **Components & Actions** — `references/components-actions.md` - @display decorator, filters, actions, tabs, fields/widgets
+ - **Ecosystem & Compatibility** — `references/ecosystem-compat.md` - django-unfold-modal, django-unfold-markdown, Django 6.x patches
---
## References
- **GitHub**: https://github.com/unfoldadmin/django-unfold
- **Documentation**: https://unfoldadmin.com/docs
- **Demo**: https://unfoldadmin.com/