The Problem
Django admin inherits a dated, functional-but-ugly interface that requires significant custom CSS and JavaScript to modernize. Teams building internal tools or client-facing dashboards spend disproportionate time overriding default admin templates rather than focusing on business logic.
What This Does
django-unfold is a modern theme that upgrades the Django admin with Tailwind CSS-styled components, dark mode, sidebar navigation, and advanced UI features. The core source lives in src/unfold/ (319 files), with static assets at src/unfold/static/unfold/js/app.js and templates at src/unfold/templates/admin/index.html. Functionality extends through contrib modules: filters in src/unfold/contrib/filters/admin/ (choice, dropdown, datetime, numeric filters), forms in src/unfold/contrib/forms/, inlines in src/unfold/contrib/inlines/, and integrations for django-constance, guardian, import-export, and simple-history. The entry point for the admin site is src/unfold/admin.py, which defines media, changelist_view, get_list_display, get_fieldsets, and get_custom_urls. The admin site wiring runs through src/unfold/sites.py, which exposes each_context, index, get_urls, and login_not_required — the latter performs a cryptographic or secret-generating operation.
How It Is Wired
Execution starts at the Django admin site registration, routing through src/unfold/admin.py and src/unfold/sites.py. The internal call graph shows 540 resolved call edges; UnfoldAdminSite is called from 74 places, render from 71, and UserAdmin from 37. each_context feeds _get_config (20 calls) and _get_value (5 calls), while queryset -> value and choices -> value chains appear frequently (14 and 12 calls respectively). The deepest nesting reaches 8 levels — found in src/unfold/checks.py, src/unfold/contrib/inlines/admin.py, and src/unfold/templatetags/unfold_list.py — making control flow hard to follow. Oversized files are a concern: src/unfold/static/unfold/js/app.js has 617 lines, src/unfold/templatetags/unfold.py 36 functions, and src/unfold/widgets.py 51 classes across 39 functions, all called from only 5 other files. The call hub each_context fans out to _get_config and _get_value, meaning changes to config resolution ripple widely. Fourty functions make outbound network calls or database reads; one function performs a cryptographic operation in src/unfold/sites.py.
How To Use It
Setup: Install via pip:
pip install django-unfold
Add unfold to INSTALLED_APPS before django.contrib.admin in settings.py, then wrap the admin site:
from unfold.admin import UnfoldAdminSite
site = UnfoldAdminSite(name="unfold_admin")
site.register(YourModel)
Configuration is done via settings.py — UNFOLD dict controls header, sidebar, theme, and dashboard settings. The package includes no Dockerfile; a container build would require a custom Dockerfile.
Running it: Start the Django development server:
python manage.py runserver
Visit /admin/ with the unfolded site registered.
Real-World Use
A team building an internal CRM can replace the default admin with Unfold to get a modern dashboard, dark mode toggle, and pre-styled filter widgets without writing custom CSS. For example, defining a numeric filter in src/unfold/contrib/filters/admin/numeric_filters.py and registering it on a ModelAdmin gives users a range slider UI out of the box. The inline tabs feature (docs/inlines/index.md) lets group inlines into tabbed navigation on the change form, reducing scroll depth for models with many related objects. The command palette (listed under "Latest improvements") provides quick model-wide searching — useful for admin users managing hundreds of records daily.
Code Health & Issues
- HIGH — Pin third-party GitHub Actions to commit SHAs:
.github/workflowsdeclaresastral-sh/setup-uv@v7. A tag can move, so the action running with your token and secrets is whatever its owner last pushed. Fix: replace@vNwith 40-character commit SHAs and let Dependabot bump them. - MEDIUM — Declare least-privilege permissions for GITHUB_TOKEN: 2 workflows in
.github/workflows/lint.ymldeclare no permissions. Without declaration the token inherits the repository default, allowing any injected step to push commits. Fix: addpermissions: contents: readat the top of each workflow. - MEDIUM — Gate pull requests on a dependency vulnerability scan: no dependency scan exists in CI. Fix: add
dependency-review-actiononpull_requestorosv-scanneron push and schedule. - MEDIUM — Set
persist-credentials: falseon checkout:checkoutkeeps the token, then dependencies are installed. The token stays in.git/configfor later steps, so a malicious postinstall script could read pushable credentials. Fix: addwith: persist-credentials: falseand pass an explicit token only to the push step. - LOW — Set
timeout-minuteson workflow jobs: 3 workflows in.github/workflows/lint.ymldeclare no job timeout. A wedged step runs to the six-hour platform default, risking overlapping runs on a two-hourly schedule. Fix: addtimeout-minuteswith a realistic bound to each job.
The Bottom Line
This is a solid upgrade path for Django projects that need a modern admin without building a theme from scratch. The Tailwind-based component library, dark mode, and pre-built filters (numeric, datetime, autocomplete) save significant front-end time. However, the JavaScript bundle is large (617 lines in app.js), several files are oversized for maintainability, and the CI hygiene is minimal — Action pins are loose, permissions are unspecified, and there’s no dependency scanning. Teams comfortable with Django’s admin patterns and who can tolerate the current health gaps will get value quickly; teams requiring tight security gates or long-term maintainability on the JS side should plan refactors for the oversized files and pin those GitHub Action SHAs.