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/workflows declares astral-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 @vN with 40-character commit SHAs and let Dependabot bump them.
  • MEDIUM — Declare least-privilege permissions for GITHUB_TOKEN: 2 workflows in .github/workflows/lint.yml declare no permissions. Without declaration the token inherits the repository default, allowing any injected step to push commits. Fix: add permissions: contents: read at the top of each workflow.
  • MEDIUM — Gate pull requests on a dependency vulnerability scan: no dependency scan exists in CI. Fix: add dependency-review-action on pull_request or osv-scanner on push and schedule.
  • MEDIUM — Set persist-credentials: false on checkout: checkout keeps the token, then dependencies are installed. The token stays in .git/config for later steps, so a malicious postinstall script could read pushable credentials. Fix: add with: persist-credentials: false and pass an explicit token only to the push step.
  • LOW — Set timeout-minutes on workflow jobs: 3 workflows in .github/workflows/lint.yml declare no job timeout. A wedged step runs to the six-hour platform default, risking overlapping runs on a two-hourly schedule. Fix: add timeout-minutes with 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.