How to create a Django admin theme
16 June 2025
Recently, I've been experimenting with creating a Django admin theme that replicates the look and feel of the Windows 95 UI. Called 'Django 95', the theme modifies the admin like so:

It's still a work in progress, and wouldn't yet be recommended for production use, but, for those who are curious, I've released it on GitHub.
As an ongoing reference, this post, and some (potential...) future posts, will document the theme's creation. To begin, this post discusses the overall process, stepping through setup etc., and focuses predominantly on the theme's look.
Note: at the time of writing, Django 5.2 is both the latest stable release and the latest LTS release.
Steps
- Set up a new Django project
- Create an app for the custom theme
- Locate Django's default admin templates and static files (CSS, JavaScript, and images)
- Customise Django's admin templates
- Add custom CSS, JavaScript, and images
1. Set up a new Django project
For the purposes of this post, I will use a new Django 5.2 project with the following structure:
django5.2/
├── core/
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
2. Create an app for the custom theme
To keep things tidy, all of the custom theme's files will be placed in a new app called django_95
.
To do so, first create an app:
python manage.py startapp django_95
django5.2/
├── core/
├── django_95/
│ ├── migrations/
│ ├── admin.py
│ ├── apps.py
│ └── etc.
└── manage.py
Then add the new app to INSTALLED_APPS
.
Note: django_95
must be added before django.contrib.admin
. This is necessary to allow any admin template override files in the django_95
app to take priority. (For further details about template loading see Django's 'How to override templates' documentation and LearnDjango's template structure tutorial)
INSTALLED_APPS = [
'django_95',
'django.contrib.admin',
...
]
3. Locate Django's default admin templates and static files
By default, Django's contrib admin app, which provides the core admin functionality, also includes an accompanying admin theme that contains the HTML, CSS, JavaScript, and image files we want to override.
3.1 Admin HTML templates
As noted in Django's documentation:
The admin template files are located in the django/contrib/admin/templates/admin
directory.
Therein, key templates include:
base.html
: The primary layout template for the admin interface, which defines the overall structure (<html>
,<head>
,<body>
, etc.).index.html
: The dashboard template displaying installed apps and models.change_form.html
: The template for editing model instances.
3.2 Static files (CSS, JavaScript, and images)
The static files are similarly located in the django/contrib/admin/static/admin
directory, with subdirectories for css
, img
, and js
.
The contents of these directories are largely self-explanatory, but it is worth noting that:
- both the
css
andjs
directories containvendor
subdirectories. - the
js
directory also contains anadmin
subdirectory, which, in turn, containsDateTimeShortcuts.js
andRelatedObjectLookups.js
files. (For reference, the arrangement of these files stretches back to at least 2011).
4. Customise Django's default admin templates
Arguably, the simplest method of either overriding or replacing a default admin template file is to:
- Locate the relevant admin template file
- Duplicate it
- Move the duplicate file to the custom theme
- Modify the duplicate file as required
Because Django 95 keeps all of its files within the django_95
app directory (rather than, for example, using a project-level templates
directory), this means that any duplicate template files should be moved to a matching directory path within the django_95
app.
For example, given the default base.html
admin template located at django/contrib/admin/templates/admin/base.html
, the duplicate file should be moved to django_95/templates/admin/base.html
.
This gives a directory structure of:
django5.2/
├── core/
├── django_95/
│ ├── migrations/
│ ├── templates/
│ │ └── admin/
│ │ └── base.html
│ ├── admin.py
│ ├── apps.py
│ └── etc.
└── manage.py
4.1 Overriding versus replacing templates
As per Django’s documentation regarding 'Overriding vs. replacing an admin template', it is recommended, where possible, to override specific parts of the contrib admin app's templates rather than replacing an entire template.
Django 95 uses both approaches as relevant, depending upon the complexity of the change and the feasibility of overriding instead of replacing a template file. For example, Django 95 overrides the default base_site.html
template (in order to change only the branding block's markup) but replaces base.html
(in order to change many aspects, a number of which fall outside of a block
).
As Django 95's base_site.html
demonstrates, the process of overriding requires that a custom template extends a default admin template:
{% extends "admin/base_site.html" %}
{% block branding %}
<h2 id="site-name">
<a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a>
</h2>
{% endblock %}
The resulting template usage can then be inspected via the 'Templates' section in Django Debug Toolbar, which now shows:
admin/base_site.html
<localpath>/django5.2/django_95/templates/admin/base_site.html
admin/base_site.html
<localpath>/django5.2/.venv/lib/<pythonversion>/site-packages/django/contrib/admin/templates/admin/base_site.html
admin/base.html
<localpath>/django5.2/django_95/templates/admin/base.html
5. Add custom CSS, JavaScript, and images
Django's default admin theme loads CSS, JavaScript, and images via various means, including:
i) Linking to a specific file directly from within a template
<link rel="stylesheet" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}">
<script src="{% static "admin/js/theme.js" %}"></script>
ii) Providing an extrastyle
block into which either linked files or raw code can be inserted
{% block extrastyle %}{% endblock %}
iii) Adding images via CSS
ul.messagelist li {
background: var(--message-success-bg) url(../img/icon-yes.svg) 40px 12px no-repeat;
...
}
iv) Adding images, in part, via Python
def _boolean_icon(field_val):
icon_url = static(
"admin/img/icon-%s.svg" % {True: "yes", False: "no", None: "unknown"}[field_val]
)
return format_html('<img src="{}" alt="{}">', icon_url, field_val)
The exact means of overriding a specific static file, or part of a file, will therefore vary depending upon the context.
5.1 Static directory structure
Django 95 mirrors the css
, img
, and js
static directory structure used by the default admin theme. Thus a path of django/contrib/admin/static/admin/css|img|js
becomes django_95/static/django_95/css|img|js
, giving the tree:
django5.2/
├── core/
├── django_95/
│ ├── migrations/
│ ├── static/
│ │ └── django_95/
│ │ ├── css/
│ │ ├── img/
│ │ └── js/
│ ├── templates/
│ ├── admin.py
│ ├── apps.py
│ └── etc.
└── manage.py
5.2 Add custom CSS
Currently, Django 95 implements a single colour scheme based upon Windows 95's 'Windows Standard' colour scheme, which features the iconic teal desktop.
As noted in Django's documentation regarding admin 'Theming support', one method of modifying the CSS is to use the default extrastyle
block mentioned above. However, because Django 95 significantly modifies the admin styling, I found it simpler to replace the CSS entirely rather than trying to override a large amount of it. This required, for example, replacing the loading of a default admin CSS file with an equivalent file from Django 95's static css
directory:
<link rel="stylesheet" href="{% block stylesheet %}{% static "admin/css/base.css" %}{% endblock %}">
<link rel="stylesheet" href="{% block stylesheet %}{% static "django_95/css/base.css" %}{% endblock %}">
5.3 Add custom JavaScript
By contrast to the custom implementation of CSS, Django 95 avoids overriding or replacing any of the default admin JavaScript. Instead, I favoured appending relevant functionality where necessary, such as that related to the start menu and windows, which helps to mimic the Windows 95 UI.
Note: even Django 95's form.js
file does not actually replace the default admin's form.js
, but, rather, modifies the markup, though not the behaviour, of various fiddly form elements, typically in order to add classes to assist with CSS.
As with adding CSS, custom JavaScript files are linked to directly from a template file, for example:
<script src="{% static "django_95/js/window.js" %}" defer></script>
5.4 Add custom images
Finally, Django 95 uses custom images which are stored in the theme's static img
directory, located at django_95/static/django_95/img
. Almost all of these images are icons, and are defined in the theme's icons.css
file. Loading a particular icon then requires applying the relevant CSS classes to the markup. For example:
<a href="{% url 'admin:login' %}"><span class="icon icon__keys"></span>{% translate 'Admin' %}</a>
Conclusion
Hopefully, this post provides a clear overview of how I've approached creating a custom Django admin theme. There's still a lot more that could be added to Django 95 (multiple colour schemes, resizeable windows, etc.), but, in the meantime, please do try it out and let me know if you have any feedback. Thanks!