name: unfold-admin description: "Django Unfold admin theme - build, configure, and enhance modern Django admin interfaces with Unfold. Use when working with: (1) Django admin UI customisation or theming, (2) Unfold ModelAdmin, inlines, actions, filters, widgets, or decorators, (3) Admin dashboard components and KPI cards, (4) Sidebar navigation, tabs, or conditional fields, (5) Any mention of 'unfold', 'django-unfold', or 'unfold admin'. Covers the full Unfold feature set: site configuration, actions system, display decorators, filter types, widget overrides, inline variants, dashboard components, datasets, sections, theming, and third-party integrations." license: MIT metadata:
Modern Django admin theme with Tailwind CSS, HTMX, and Alpine.js. Replaces Django's default admin with a polished, feature-rich interface.
# settings.py - unfold MUST be before django.contrib.admin
INSTALLED_APPS = [
"unfold",
"unfold.contrib.filters", # advanced filters
"unfold.contrib.forms", # array/wysiwyg widgets
"unfold.contrib.inlines", # nonrelated inlines
"unfold.contrib.import_export", # styled import/export
"unfold.contrib.guardian", # django-guardian integration
"unfold.contrib.simple_history", # django-simple-history integration
"unfold.contrib.constance", # django-constance integration
"unfold.contrib.location_field", # django-location-field integration
# ...
"django.contrib.admin",
]
from unfold.admin import ModelAdmin
@admin.register(MyModel)
class MyModelAdmin(ModelAdmin):
pass # inherits Unfold styling
Replace the default AdminSite or configure via UNFOLD dict in settings. See references/configuration.md for the complete settings reference.
UNFOLD = {
"SITE_TITLE": "My Admin",
"SITE_HEADER": "My Admin",
"SITE_SYMBOL": "dashboard", # Material Symbols icon name
"SIDEBAR": {
"show_search": True,
"navigation": [
{
"title": _("Navigation"),
"items": [
{
"title": _("Dashboard"),
"icon": "dashboard",
"link": reverse_lazy("admin:index"),
},
],
},
],
},
}
When building Unfold admin interfaces, follow this sequence:
unfold.admin.ModelAdmin@display decorator for list columns@action decorator for row/list/detail/submit actionsformfield_overrides@register_component + BaseComponent for KPI cardsUnfold extends Django's ModelAdmin with these additional attributes:
| Attribute | Type | Purpose |
|---|---|---|
list_fullwidth |
bool | Full-width changelist (no sidebar) |
list_filter_submit |
bool | Add submit button to filters |
list_filter_sheet |
bool | Filters in sliding sheet panel |
compressed_fields |
bool | Compact field spacing in forms |
warn_unsaved_form |
bool | Warn before leaving unsaved form |
ordering_field |
str | Field name for drag-to-reorder |
hide_ordering_field |
bool | Hide the ordering field column |
list_horizontal_scrollbar_top |
bool | Scrollbar at top of list |
list_disable_select_all |
bool | Disable "select all" checkbox |
change_form_show_cancel_button |
bool | Show cancel button on form |
actions_list |
list | Global changelist actions |
actions_row |
list | Per-row actions in changelist |
actions_detail |
list | Actions on change form |
actions_submit_line |
list | Actions in form submit area |
actions_list_hide_default |
bool | Hide default list actions |
actions_detail_hide_default |
bool | Hide default detail actions |
conditional_fields |
dict | JS expressions for field visibility |
change_form_datasets |
list | BaseDataset subclasses for change form |
list_sections |
list | TableSection/TemplateSection for list |
list_sections_classes |
str | CSS grid classes for sections |
readonly_preprocess_fields |
dict | Transform readonly field content |
add_fieldsets |
list | Separate fieldsets for add form (like UserAdmin) |
Insert custom HTML before/after changelist or change form:
class MyAdmin(ModelAdmin):
# Changelist
list_before_template = "myapp/list_before.html"
list_after_template = "myapp/list_after.html"
# Change form (inside <form> tag)
change_form_before_template = "myapp/form_before.html"
change_form_after_template = "myapp/form_after.html"
# Change form (outside <form> tag)
change_form_outer_before_template = "myapp/outer_before.html"
change_form_outer_after_template = "myapp/outer_after.html"
Show/hide fields based on other field values (Alpine.js expressions):
class MyAdmin(ModelAdmin):
conditional_fields = {
"premium_features": "plan == 'PRO'",
"discount_amount": "has_discount == true",
}
Four action types, each with different signatures. See references/actions-filters.md for complete reference.
from unfold.decorators import action
from unfold.enums import ActionVariant
# List action (no object context)
@action(description=_("Rebuild Index"), icon="sync", variant=ActionVariant.PRIMARY)
def rebuild_index(self, request):
# process...
return redirect(request.headers["referer"])
# Row action (receives object_id)
@action(description=_("Approve"), url_path="approve")
def approve_row(self, request, object_id):
obj = self.model.objects.get(pk=object_id)
return redirect(request.headers["referer"])
# Detail action (receives object_id, shown on change form)
@action(description=_("Send Email"), permissions=["send_email"])
def send_email(self, request, object_id):
return redirect(reverse_lazy("admin:myapp_mymodel_change", args=[object_id]))
# Submit line action (receives obj instance, runs on save)
@action(description=_("Save & Publish"))
def save_and_publish(self, request, obj):
obj.published = True
actions_list = [
"primary_action",
{
"title": _("More"),
"variant": ActionVariant.PRIMARY,
"items": ["secondary_action", "tertiary_action"],
},
]
@action(permissions=["can_export", "auth.view_user"])
def export_data(self, request):
pass
def has_can_export_permission(self, request):
return request.user.is_superuser
Enhance list_display columns. See references/actions-filters.md.
from unfold.decorators import display
# Colored status labels
@display(description=_("Status"), ordering="status", label={
"active": "success", # green
"pending": "info", # blue
"warning": "warning", # orange
"inactive": "danger", # red
})
def show_status(self, obj):
return obj.status
# Rich header with avatar
@display(description=_("User"), header=True)
def show_header(self, obj):
return [
obj.full_name, # primary text
obj.email, # secondary text
obj.initials, # badge text
{"path": obj.avatar.url, "width": 24, "height": 24, "borderless": True},
]
# Interactive dropdown
@display(description=_("Teams"), dropdown=True)
def show_teams(self, obj):
return {
"title": f"{obj.teams.count()} teams",
"items": [{"title": t.name, "link": t.get_admin_url()} for t in obj.teams.all()],
"striped": True,
"max_height": 200,
}
# Boolean checkmark
@display(description=_("Active"), boolean=True)
def is_active(self, obj):
return obj.is_active
Unfold provides advanced filter classes. See references/actions-filters.md.
from unfold.contrib.filters.admin import (
TextFilter, RangeNumericFilter, RangeDateFilter, RangeDateTimeFilter,
SingleNumericFilter, SliderNumericFilter, RelatedDropdownFilter,
RelatedCheckboxFilter, ChoicesCheckboxFilter, AllValuesCheckboxFilter,
BooleanRadioFilter, CheckboxFilter, AutocompleteSelectMultipleFilter,
)
class MyAdmin(ModelAdmin):
list_filter_submit = True # required for input-based filters
list_filter = [
("salary", RangeNumericFilter),
("status", ChoicesCheckboxFilter),
("created_at", RangeDateFilter),
("category", RelatedDropdownFilter),
("is_active", BooleanRadioFilter),
]
class NameFilter(TextFilter):
title = _("Name")
parameter_name = "name"
def queryset(self, request, queryset):
if self.value() in EMPTY_VALUES:
return queryset
return queryset.filter(name__icontains=self.value())
Override form widgets for Unfold styling. See references/widgets-inlines.md.
from unfold.widgets import (
UnfoldAdminTextInputWidget, UnfoldAdminSelectWidget, UnfoldAdminSelect2Widget,
UnfoldBooleanSwitchWidget, UnfoldAdminColorInputWidget,
UnfoldAdminSplitDateTimeWidget, UnfoldAdminImageFieldWidget,
)
from unfold.contrib.forms.widgets import WysiwygWidget, ArrayWidget
class MyAdmin(ModelAdmin):
formfield_overrides = {
models.TextField: {"widget": WysiwygWidget},
models.ImageField: {"widget": UnfoldAdminImageFieldWidget},
}
widget = UnfoldAdminTextInputWidget(attrs={
"prefix_icon": "search",
"suffix_icon": "euro",
})
Unfold inlines support tabs, pagination, sorting, and nonrelated models. See references/widgets-inlines.md.
from unfold.admin import TabularInline, StackedInline
from unfold.contrib.inlines.admin import NonrelatedStackedInline
class OrderItemInline(TabularInline):
model = OrderItem
tab = True # show as tab
per_page = 10 # paginated
ordering_field = "weight" # drag-to-reorder
hide_title = True
collapsible = True
Group fieldsets into tabs using "classes": ["tab"]:
fieldsets = [
(None, {"fields": ["name", "email"]}), # always visible
(_("Profile"), {"classes": ["tab"], "fields": ["bio", "avatar"]}),
(_("Settings"), {"classes": ["tab"], "fields": ["theme", "notifications"]}),
]
Build KPI cards and custom dashboard widgets. See references/dashboard.md.
from unfold.components import BaseComponent, register_component
from django.template.loader import render_to_string
@register_component
class ActiveUsersComponent(BaseComponent):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["children"] = render_to_string("myapp/kpi_card.html", {
"total": User.objects.filter(is_active=True).count(),
"label": "Active Users",
})
return context
Configure in settings:
UNFOLD = {
"DASHBOARD_CALLBACK": "myapp.views.dashboard_callback",
}
Embed related data panels in changelist views:
from unfold.sections import TableSection, TemplateSection
class RecentOrdersSection(TableSection):
related_name = "order_set"
fields = ["id", "total", "status"]
height = 380
class ChartSection(TemplateSection):
template_name = "myapp/chart.html"
class MyAdmin(ModelAdmin):
list_sections = [RecentOrdersSection, ChartSection]
list_sections_classes = "lg:grid-cols-2"
Embed model listings within change forms:
from unfold.datasets import BaseDataset
class RelatedItemsDatasetAdmin(ModelAdmin):
list_display = ["name", "status"]
search_fields = ["name"]
class RelatedItemsDataset(BaseDataset):
model = RelatedItem
model_admin = RelatedItemsDatasetAdmin
tab = True # show as tab
class MyAdmin(ModelAdmin):
change_form_datasets = [RelatedItemsDataset]
Use infinite scroll pagination:
from unfold.paginator import InfinitePaginator
class MyAdmin(ModelAdmin):
paginator = InfinitePaginator
show_full_result_count = False
list_per_page = 20
Unfold provides styled wrappers for common Django packages. See references/resources.md for complete setup guides.
| Package | Unfold Module | Setup |
|---|---|---|
| django-import-export | unfold.contrib.import_export |
Use ImportForm, ExportForm, SelectableFieldsExportForm |
| django-guardian | unfold.contrib.guardian |
Styled guardian integration |
| django-simple-history | unfold.contrib.simple_history |
Styled history integration |
| django-constance | unfold.contrib.constance |
Styled constance config |
| django-location-field | unfold.contrib.location_field |
Location widget |
| django-modeltranslation | Compatible | Mix TabbedTranslationAdmin with ModelAdmin |
| django-celery-beat | Compatible (rewire) | Unregister 5 models, re-register with Unfold |
| django-money | unfold.widgets |
UnfoldAdminMoneyWidget |
| djangoql | Compatible | Mix DjangoQLSearchMixin with ModelAdmin |
| django-crispy-forms | Compatible | Unfold template pack available |
# Multiple inheritance - Unfold ModelAdmin always last
@admin.register(MyModel)
class MyAdmin(DjangoQLSearchMixin, SimpleHistoryAdmin, GuardedModelAdmin, ModelAdmin):
pass
Unfold ships reusable template components for dashboards and custom pages:
| Component | Path | Key Variables |
|---|---|---|
| Card | unfold/components/card.html |
title, footer, label, icon |
| Bar Chart | unfold/components/chart/bar.html |
data (JSON), height, width |
| Line Chart | unfold/components/chart/line.html |
data (JSON), height, width |
| Progress | unfold/components/progress.html |
value, title, description |
| Table | unfold/components/table.html |
table, card_included, striped |
| Button | unfold/components/button.html |
name, href, submit |
| Tracker | unfold/components/tracker.html |
data |
| Cohort | unfold/components/cohort.html |
data |
{% load unfold %}
{% component "MyKPIComponent" %}{% endcomponent %}
Unfold provides styled versions of Django's auth admin forms:
from unfold.forms import AdminPasswordChangeForm, UserChangeForm, UserCreationForm
@admin.register(User)
class UserAdmin(BaseUserAdmin, ModelAdmin):
form = UserChangeForm
add_form = UserCreationForm
change_password_form = AdminPasswordChangeForm
Detailed documentation split by topic:
Read the relevant reference file when you need detailed configuration options, the full list of available classes, complete code examples, or integration setup guides for a specific feature area.
| Resource | URL |
|---|---|
| Docs | https://unfoldadmin.com/docs/ |
| GitHub | https://github.com/unfoldadmin/django-unfold |
| Demo App (Formula) | https://github.com/unfoldadmin/formula |
| Live Demo | https://demo.unfoldadmin.com |
| Material Symbols (Icons) | https://fonts.google.com/icons |
When uncertain about an implementation pattern, consult formula/admin.py and formula/settings.py in the Formula demo repo - it covers virtually every Unfold feature.