Django Unfold
Modern Django admin theme with beautiful design and advanced features.
Overview
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.
Installation
pip install django-unfold
# Or with specific version
pip install django-unfold==0.9.0
# 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
}
Important:
unfoldmust be first inINSTALLED_APPSto override Django templates.
Quick Start
Basic Configuration
# 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:
# urls.py - No changes needed!
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# ... other urls
]
Settings Reference
Site Branding
UNFOLD = {
'SITE_HEADER': 'My Company Admin',
'SITE_TITLE': 'My Admin',
'INDEX_TITLE': 'Welcome to Dashboard',
# Or with HTML support
'SITE_HEADER': '<div class="flex items-center gap-2"><span>🚀</span> My Company</div>',
}
Colors and Theme
UNFOLD = {
'COLORS': {
'primary': {
'50': '239 246 255',
'100': '219 234 254',
'200': '191 219 254',
'300': '147 197 253',
'400': '96 165 250',
'500': '59 130 246',
'600': '37 99 235',
'700': '29 78 216',
'800': '30 64 175',
'900': '30 58 138',
'950': '30 58 138',
},
},
'DARK_MODE_COLORS': {
'primary': {
'50': '239 246 255',
# ... dark mode colors
},
},
}
Sidebar Navigation
UNFOLD = {
'SIDEBAR': {
'show_search': True,
'navigation': [
{
'title': 'Main',
'items': [
{'title': 'Dashboard', 'icon': 'dashboard', 'link': '/admin/'},
{'title': 'Users', 'icon': 'people', 'link': '/admin/auth/user/'},
],
},
{
'title': 'Content',
'items': [
{'title': 'Articles', 'icon': 'article', 'link': '/admin/myapp/article/'},
],
},
],
},
}
Dashboard Widgets
UNFOLD = {
'DASHBOARD_WIDGETS': [
'unfold.widgets.DashboardStatistics',
'unfold.widgets.DashboardActions',
# Custom widgets
],
}
Feature Flags
UNFOLD = {
# Show/hide features
'SHOW_HISTORY': True,
'SHOW_VIEW_ON_SITE': True,
# Disable specific features
'AUTH_PASSWORD_VALIDATION': True,
}
Custom Admin Site
Custom Site Class
# admin.py
from unfold.admin import ModelAdmin, UnfoldAdminSite
from django.contrib.admin import AdminSite
class CustomAdminSite(UnfoldAdminSite):
site_header = 'My Custom Admin'
site_title = 'My Admin Panel'
index_title = 'Welcome to Management'
def each_context(self, request):
context = super().each_context(request)
# Add custom context
context['custom_data'] = 'value'
return context
admin_site = CustomAdminSite(name='myadmin')
# settings.py
UNFOLD_ADMIN_SITE_CLASS = 'myapp.admin.CustomAdminSite'
Custom ModelAdmin
# admin.py
from django.contrib import admin
from unfold.admin import ModelAdmin
from .models import Article
@admin.register(Article, site=custom_admin_site)
class ArticleAdmin(ModelAdmin):
list_display = ['title', 'status', 'created_at']
search_fields = ['title', 'content']
list_filter = ['status', 'created_at']
# Unfold-specific
sidebar_fieldsets = (
(None, {'fields': ('title', 'slug')}),
('Content', {'fields': ('content', 'excerpt')}),
)
Components
Buttons
from unfold.decorators import action
from unfold.actions import Actions
class ArticleAdmin(ModelAdmin):
@action(description='Publish selected')
def make_published(self, request, queryset):
queryset.update(status='published')
@action(description='Export to CSV')
def export_csv(self, request, queryset):
# Export logic
pass
@display Decorator (Django 6.0+)
from unfold.decorators import display
class UserAdmin(ModelAdmin):
@display(description="Status", boolean=True)
def is_active(self, obj):
return obj.is_active
@display(description="Actions", order="username")
def user_actions(self, obj):
return f"{obj.first_name} {obj.last_name}"
Cards
# In change_view.html or custom template
{% load unfold %}
{% component "card" title="Statistics" %}
<div class="p-4">
<p class="text-2xl font-bold">1,234</p>
<p class="text-gray-500">Total Users</p>
</div>
{% endcomponent %}
Charts
# Using Unfold chart component
{% component "chart" type="line" data=chart_data %}
{% endcomponent %}
Tables
# In list_display
class UserAdmin(ModelAdmin):
list_display = ['username', 'email', 'status_badge']
@bind_to(admin_order_field='is_active')
def status_badge(self, obj):
from unfold.helpers import icon
if obj.is_active:
return icon('check_circle', classes='text-green-500')
return icon('x_circle', classes='text-red-500')
Fields and Widgets
Autocomplete Fields
from unfold import fields
from unfold.forms import ModelForm
class ArticleForm(ModelForm):
class Meta:
model = Article
fields = '__all__'
author = fields.AutocompleteField(
queryset=User.objects.all(),
search_fields=['username', 'email'],
label='Author'
)
JSON Fields
from unfold.fields import JSONField
class ConfigAdmin(ModelAdmin):
fieldsets = (
(None, {
'fields': ('config_json',)
}),
)
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
form.base_fields['config_json'] = fields.JSONField(
widget=forms.Textarea(attrs={
'class': 'font-mono text-sm',
'rows': 10
})
)
return form
Filters
Custom Filters
import unfold.filters as filters
class ArticleAdmin(ModelAdmin):
list_filter = [
('status', filters.DropdownFilter),
('category', filters.DropdownFilter),
('created_at', filters.DateRangeFilter),
('author', filters.AutocompleteFilter),
]
Filter Types
# Dropdown filter
('status', filters.DropdownFilter)
# Date range filter
('created_at', filters.DateRangeFilter)
# Autocomplete filter (for FK/M2M with many items)
('author', filters.AutocompleteFilter)
# Checkbox/radio filter
('is_published', filters.CheckboxFilter)
# Numeric range filter
('views', filters.NumericFilter)
# Text search filter
('title', filters.TextFilter)
Actions
Custom Actions
from django.http import HttpResponse
from django.shortcuts import render
import csv
import io
class ArticleAdmin(ModelAdmin):
@action(description='Export selected to CSV')
def export_csv(self, request, queryset):
# Create CSV
buffer = io.StringIO()
writer = csv.writer(buffer)
writer.writerow(['Title', 'Status', 'Created'])
for obj in queryset:
writer.writerow([obj.title, obj.status, obj.created])
# Return response
response = HttpResponse(buffer.getvalue(), content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="articles.csv"'
return response
@action(description='Send to publication')
def publish(self, request, queryset):
queryset.update(status='published', published_at=timezone.now())
publish.short_description = 'Publish selected articles'
Row Actions
class ArticleAdmin(ModelAdmin):
def get_row_actions(self, obj):
actions = super().get_row_actions(obj)
actions.append(
actions.Link(
'preview',
icon='visibility',
link=f'/admin/myapp/article/{obj.pk}/preview/'
)
)
return actions
Tabs
Using Tabs
class ArticleAdmin(ModelAdmin):
tabs = [
{'title': 'Content', 'id': 'content'},
{'title': 'SEO', 'id': 'seo'},
{'title': 'Metadata', 'id': 'metadata'},
]
fieldsets = (
(None, {
'fields': ('title', 'content'),
'tab_id': 'content',
}),
('SEO Settings', {
'fields': ('meta_title', 'meta_description'),
'tab_id': 'seo',
}),
('Metadata', {
'fields': ('created_at', 'updated_at'),
'tab_id': 'metadata',
}),
)
Integrations
django-guardian (Object Permissions)
# 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
django-import-export
pip install django-import-export
# Works automatically with Unfold
# Custom resource if needed
from import_export import resources
from unfold.admin import ImportExportMixin
class ArticleResource(resources.ModelResource):
class Meta:
model = Article
fields = ('id', 'title', 'status', 'created_at')
class ArticleAdmin(ImportExportMixin, ModelAdmin):
resource_classes = [ArticleResource]
django-celery-beat
# Install
pip install django-celery-beat
# Configuration is automatic with Unfold
# Shows schedule in admin dashboard
django-constance
pip install django-constance[database]
INSTALLED_APPS = [
'constance',
'unfold',
# ...
]
# Configuration is automatic
# Shows in admin under "Constance" section
Authentication Customization
Custom Login Form
# Custom form for Unfold login
from django import forms
from unfold.forms import LoginForm
class CustomLoginForm(LoginForm):
def clean(self):
# Add custom validation
cleaned_data = super().clean()
# Custom logic
return cleaned_data
# settings.py
UNFOLD = {
'LOGIN': {
'form': 'myapp.forms.CustomLoginForm',
},
}
Custom Views
# Custom password reset
UNFOLD = {
'PASSWORD_CHANGE_FORM': 'myapp.forms.CustomPasswordChangeForm',
'PASSWORD_RESET_FORM': 'myapp.forms.CustomPasswordResetForm',
}
Dark Mode
Automatic Dark Mode
Unfold automatically supports dark mode based on system preference.
Manual Control
# Force dark mode
UNFOLD = {
'THEME': 'dark', # 'dark' or 'light' or None (auto)
}
Custom Colors for Dark Mode
UNFOLD = {
'DARK_MODE_COLORS': {
'primary': {
'50': '224 242 254',
'100': '214 238 253',
# ... custom dark mode colors
},
},
}
Custom Styling
Custom CSS
# settings.py
UNFOLD = {
'STYLES': [
'css/custom.css',
],
}
# templates/admin/base.html
{% extends "admin/base.html" %}
{% load static %}
{% block extrastyle %}
<link rel="stylesheet" href="{% static 'css/custom.css' %}">
{% endblock %}
Custom JavaScript
UNFOLD = {
'SCRIPTS': [
'js/custom.js',
],
}
Best Practices
- Keep unfold first in INSTALLED_APPS
- Use Unfold ModelAdmin for all models
- Leverage tabs for organized forms
- Use filters for better list navigation
- Customize actions for bulk operations
- Enable dark mode - users love it
- Use integrations - they work out of the box
Ecosystem Libraries
django-unfold-modal
- URL: https://github.com/metaforx/django-unfold-modal
- PyPI:
django-unfold-modal - Version: 0.2.1 (requires Python 3.10+, Django 5.0+, django-unfold 0.52.0+)
Replaces Django admin's popup windows for related objects (ForeignKey, ManyToMany, etc.) with Unfold-styled modals, so related-object selection happens in-page instead of in separate browser windows.
Key features:
- Modal replacement for admin related-object popups (ForeignKey/ManyToMany/OneToOne selects,
raw_id_fieldslookup,autocomplete_fields/Select2, related fields inside inlines) - Nested modals with replace/restore behavior
- Size presets (
default/large/full) and optional manual resize handle - Optional admin header suppression inside modal iframes
- Django CMS integration — admin modals can open in the CMS parent window when admin runs inside a CMS modal
pip install django-unfold-modal
django-unfold-markdown
- URL: https://github.com/sergei-vasilev-dev/django-unfold-markdown
- PyPI:
django-unfold-markdown - Version: 0.1.2 (requires Python >=3.10, Django >=4.2, django-unfold >=0.70.0)
Markdown editor widget for the Django Unfold admin: a plain-text monospace editor with live preview that renders stored content as Markdown.
Key features:
- Plain text editor with monospace font (no rich-text WYSIWYG)
- Side-by-side live preview and fullscreen mode
- Dark/light theme integration with Unfold; Material Symbols icons matching Unfold design
- Toolbar: bold, italic, strikethrough, headings, lists, links, images, tables, horizontal rule
- No autosave — content saves on form submit
- Responsive layout; renders stored text with
markdownormistune
pip install django-unfold-markdown
django-unfold-extra
- URL: https://github.com/metaforx/django-unfold-extra
- PyPI:
django-unfold-extra - Version: 0.4.0 (requires
django-unfold>=0.92,django-cms>=5.0,<5.1,django-parler~=2.3)
Unofficial "extra" package that enhances the Django Unfold admin interface with Unfold-styled integrations for Django CMS 5.0, django-parler multilingual models, and django-versatileimagefield.
Key features:
- Django CMS 5.0 integration: Unfold-styled page tree with "New Page" button and language switcher, tabbed Page/PageContent change forms,
PageUser/PageUserGroup/GlobalPagePermissionadmin with permission inlines,djangocms-versioningadmin UnfoldCMSPluginBasewithUnfoldStackedInline/UnfoldTabularInline; drop-in Unfold-styleddjangocms-linkLink plugin anddjangocms-aliasadmin- django-parler support:
UnfoldTranslatableAdminMixin,UnfoldTranslatableStackedAdminMixin,UnfoldTranslatableTabularAdminMixin, translatable inlines - django-versatileimagefield integration (preview + ppoi)
- Unfold auto-update of styles from the official package via npm; Theme-Sync to control themes from either the Unfold or CMS switcher
pip install django-unfold-extra
Note: Used in production but the author notes to "expect additional implementation work"; recommended mainly when most CMS plugins are custom-built.
django-admin-action-forms
- URL: https://github.com/michalpokusa/django-admin-action-forms
- PyPI:
django-admin-action-forms - Version: 3.0.0 (supports Django 3.2.x through 6.x.x; requires only
django>=3.2)
Django admin extension that adds confirmation pages for admin actions: each action can have a custom form shown on an intermediate page before execution, with the form data passed to the action as an extra argument. Works with the stock Django admin (not Unfold-specific, but pairs well with Unfold's admin theming).
Key features:
@action_with_formdecorator +ActionFormbase class; form data passed to the action as an additional argument- Supports Django 3.2.x through 6.x.x; no additional dependencies
- Supports
fields/fieldsets,filter_horizontal/filter_vertical,autocomplete_fields, custom widgets and validators - Formset support via inlines (
StackedAdminActionInline,TabularAdminActionInline) - Easily testable with Django's built-in testing tools; compatible with
django-no-queryset-admin-actions
pip install django-admin-action-forms
Add 'django_admin_action_forms' to INSTALLED_APPS.
Django 6.x get_action_choices Compatibility
django-unfold 0.105.0's ModelAdmin.get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH) does not accept the action_location argument that Django 6.x requires, triggering a DeprecationWarning that fails the test suite when warnings are treated as errors.
Since this is an upstream bug, the fix is a monkey-patch in an AppConfig.ready() hook:
# customers/apps.py
from django.apps import AppConfig
class CustomersConfig(AppConfig):
name = "customers"
def ready(self):
from unfold.admin import ModelAdmin
original = ModelAdmin.get_action_choices
def patched(self, request, default_choices=None, action_location=None):
return original(self, request, default_choices)
ModelAdmin.get_action_choices = patched
This runs once at Django startup and silences the warning for the entire process. Remove the patch when django-unfold ships a version that accepts action_location natively.
INSTALLED_APPS Ordering
django-unfold must appear in INSTALLED_APPS before django.contrib.admin so its templates and static files take precedence:
INSTALLED_APPS = [
# ...
"unfold", # before admin
"django.contrib.admin",
# ...
]
If the order is reversed, the admin renders with the default Django admin theme and Unfold's customizations are silently ignored.
Admin URL Precedence and Action Payload
These patterns apply to Unfold and standard Django admin alike, but surface frequently when customizing Unfold views:
Custom URLs before
super().get_urls()— A custom Unfold view like a voucher-creation wizard must be registered before the default changelist pattern, or<path:object_id>/swallows it.Action POST payload — when triggering a custom action from an Unfold view via JavaScript, the payload must include
action(the method name) and_selected_action(list of IDs). Swapping them silently no-ops the action.Redirects —
UnfoldAdminSitehas nourls_urlattribute. Usedjango.urls.reverse()to build redirect targets from custom Unfold views.
from django.urls import reverse
return redirect(reverse("admin:customers_customer_changelist"))
References
- GitHub: https://github.com/unfoldadmin/django-unfold
- Documentation: https://unfoldadmin.com/docs
- Demo: https://unfoldadmin.com/