Your organization deploys Wagtail for a client-facing CMS, but the default admin interface shows Wagtail branding everywhere. This lesson teaches you to customize every surface of the Wagtail admin: CSS theming, login page branding, custom admin views via ModelAdmin, dashboard panels, editor enhancements, and menu customization — all without modifying a single line of Wagtail's core code.
1. Learning Objectives
By the end of this lesson, you will be able to:
- Inject custom CSS and JavaScript into the Wagtail admin interface using
registerhooks - Override and brand the Wagtail login page with custom logo, colors, and typography
- Register Django models in the Wagtail admin using
ModelAdmin - Build custom admin views and dashboard panels using Wagtail's hook system
- Customize the admin sidebar menu — adding, reordering, and removing items programmatically
- Apply custom fonts and theming to match your organization's brand guidelines
- Tailor the editor experience with custom widgets, choosers, and panel configurations
2. Why This Matters (Real-World Scenario)
Your agency just deployed a Wagtail CMS for a fashion retail client. The client's content team logs in daily to manage products, blog posts, and promotional campaigns. The default Wagtail admin — with its blue Wagtail logo, generic favicon, and unbranded login page — feels like a third-party tool, not their own platform. The client has asked: "Can we make the admin look like our brand?"
This is not just vanity. Branded admin interfaces improve adoption, reduce confusion for non-technical editors, and make client-facing CMS deployments feel professional. This lesson teaches you to customize every surface: login page, favicon, color scheme, admin menu, custom management views, dashboard panels, and the editorial experience — all without modifying Wagtail's core code.
3. Core Concepts
Wagtail Hook System — Wagtail uses Python @register hooks to let you inject code into the admin lifecycle without modifying core templates. Hooks like insert_global_admin_css and insert_editor_js let you add assets globally or per-editor view. Hooks live in a wagtail_hooks.py file inside any installed Django app.
insert_global_admin_css / insert_editor_css — Return a string or list of CSS file paths that get injected into the admin <head>. The global variant applies to all admin pages, while the editor variant applies only to the page editor.
insert_editor_js / insert_global_admin_js — Same concept for JavaScript. Use for custom chooser widgets, admin interactions, or editor enhancements.
ModelAdmin — A Wagtail contrib module (wagtail.contrib.modeladmin) that lets you register any Django model in the admin sidebar with list views, create/edit forms, and search — exactly like Django's admin, but with Wagtail's look and feel. Supports custom views, buttons, and URL configuration.
Template Overriding — Wagtail admin templates live in wagtailadmin/templates/wagtailadmin/. You can override any template by placing a file with the same path in your project's templates directory. Common overrides: wagtailadmin/login.html, wagtailadmin/home.html, wagtailadmin/base.html.
Menu Hooks — construct_main_menu lets you modify the existing sidebar menu items (reorder, remove, rename). register_admin_menu_item lets you add entirely new menu items. These hooks receive the request, so you can conditionally show items based on user permissions.
Dashboard Panels — Wagtail's admin home page displays summary panels. You can register custom panels via the construct_homepage_panels hook, adding charts, stats, recent activity, or quick-action cards.
4. Hands-On Practice: Step-by-Step Admin Customization
4.1 Setting Up the wagtail_hooks.py File
All admin customizations start here. Create this file inside any installed Django app (e.g., blog/wagtail_hooks.py). Wagtail auto-discovers it.
from wagtail import hooks
from django.templatetags.static import static
def inject_admin_css():
return static('css/admin-custom.css')
def inject_editor_js():
return static('js/admin-editor.js')
Place your static files in your_app/static/css/admin-custom.css and your_app/static/js/admin-editor.js. Run python manage.py collectstatic to make them available.
4.2 Branding the Admin with Custom CSS
Custom CSS is the fastest way to brand the admin. Create a CSS file that overrides Wagtail's default variables and styles:
/* static/css/admin-custom.css */
:root {
--w-color-primary: #1a56db; /* Brand primary blue */
--w-color-primary-200: #1e40af;
--w-color-primary-hover: #1e40af;
--w-color-link: #1a56db;
--w-color-link-hover: #1e40af;
--w-color-surface-header: #1e293b; /* Dark header */
--w-color-text-header: #ffffff;
--w-color-surface-page: #f8fafc;
}
/* Custom logo replacement */
.w-logo__img {
content: url('/static/images/brand-logo.svg');
width: 140px;
height: auto;
}
/* Brand favicon */
link[rel*='icon'] {
href: '/static/images/favicon.ico';
}
Tip: Wagtail 6+ uses CSS custom properties extensively. Inspect the admin with your browser's DevTools to find the variable names used for backgrounds, text, borders, and buttons — they follow the --w-color-* naming convention.
4.3 Customizing the Login Page
The login page template lives at wagtailadmin/login.html. Override it by placing a file at the same relative path in your project templates directory:
{% extends 'wagtailadmin/login.html' %}
{% load static %}
{% block branding_logo %}
<img src="{% static 'images/brand-logo.svg' %}" alt="Brand Name"
style="max-width: 200px; height: auto; margin: 2rem auto;">
{% endblock %}
{% block branding_title %}Brand Name CMS{% endblock %}
{% block branding_login_prefix %}
<h1>Welcome to Brand Name CMS</h1>
<p class="help">Use your organization credentials to sign in.</p>
{% endblock %}
Place this at templates/wagtailadmin/login.html in your project. The branding_logo, branding_title, and branding_login_prefix blocks let you replace specific sections without rewriting the entire template.
For a custom login background or layout, combine this with CSS:
.login {
background: linear-gradient(135deg, #1a56db 0%, #7c3aed 100%);
}
.login .content-wrapper {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
padding: 3rem;
}
4.4 Registering Custom Admin Views with ModelAdmin
ModelAdmin is the simplest way to add custom management pages. It works for any Django model — not just Wagtail page models. Create a file in your app, then register it:
# blog/model_admin.py (or wagtail_hooks.py)
from wagtail.contrib.modeladmin.options import (
ModelAdmin, ModelAdminGroup, modeladmin_register
)
from .models import Product, Category
class ProductAdmin(ModelAdmin):
model = Product
menu_label = 'Products'
menu_icon = 'box'
menu_order = 200
add_to_settings_menu = False
exclude_from_explorer = False
list_display = ('name', 'price', 'category', 'is_active', 'created_at')
list_filter = ('category', 'is_active')
search_fields = ('name', 'description')
list_export = ('name', 'price', 'category__name', 'created_at')
export_filename = 'products_export'
modeladmin_register(ProductAdmin)
ModelAdmin supports: list_display, list_filter, search_fields, list_export (CSV export), custom inspect_view, create_view, and edit_view. It even supports custom buttons via list_display_add_buttons.
Group related models under a single menu heading:
class InventoryGroup(ModelAdminGroup):
menu_label = 'Inventory'
menu_icon = 'folder-open-inverse'
menu_order = 300
items = (ProductAdmin, CategoryAdmin, SupplierAdmin)
modeladmin_register(InventoryGroup)
4.5 Custom Admin Views with Hooks
For views that don't correspond to a single model (dashboards, reports, import wizards), register them via hooks:
from django.shortcuts import render
from django.urls import reverse
from wagtail.admin.menu import MenuItem
from wagtail import hooks
def report_dashboard_view(request):
"""Custom admin view showing aggregated content reports."""
return render(request, 'admin/report_dashboard.html', {
'page_title': 'Content Report',
# Your context goes here
})
def register_report_menu_item():
return MenuItem(
'Reports',
reverse('report-dashboard'),
icon_name='statistics',
order=400
)
# Construct the menu — remove, reorder, or add items
from wagtail import hooks as wagtail_hooks
def modify_admin_menu(request, menu_items):
# Remove the default 'Settings' for non-superusers
if not request.user.is_superuser:
menu_items[:] = [
item for item in menu_items
if item.name not in ('settings',)
]
return menu_items
Don't forget to wire the URL in your urls.py and create the template:
# urls.py
from django.urls import path
from . import wagtail_hooks
urlpatterns = [
path('admin/reports/', wagtail_hooks.report_dashboard_view, name='report-dashboard'),
]
Important: Custom admin views are not automatically protected by Wagtail's admin permissions. Wrap them with @permission_required or @login_required decorators to restrict access.
4.6 Dashboard Panels on the Admin Home Page
Add custom panels to the admin dashboard using the construct_homepage_panels hook:
from wagtail.admin.home import WagtailHomepage
from wagtail.admin.panels import Panel
from wagtail import hooks
from django.db.models import Count
from .models import Product, BlogPage
class StatsPanel(Panel):
name = 'content-stats'
order = 100
template_name = 'admin/panels/stats_panel.html'
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context['product_count'] = Product.objects.count()
context['blog_count'] = BlogPage.objects.live().count()
return context
def add_stats_panel(request, panels):
panels.append(StatsPanel())
return panels
Create the template at templates/admin/panels/stats_panel.html:
{% load i18n wagtailadmin_tags %}
<div class="panel nice-padding">
<h2>{% trans 'Content Overview' %}</h2>
<ul class="stats-list">
<li>
<span class="stats-number">{{ product_count }}</span>
<span class="stats-label">Products</span>
</li>
<li>
<span class="stats-number">{{ blog_count }}</span>
<span class="stats-label">Published Blog Posts</span>
</li>
</ul>
</div>
4.7 Customizing the Editor Experience
Wagtail's page editor supports custom widgets, panel types, and field layouts. Use these to tailor the editing experience for your content team:
# Custom widget for JSON/structured data
from django import forms
from wagtail.admin.widgets import AdminAutoHeightTextInput
class JSONEditorWidget(AdminAutoHeightTextInput):
"""Textarea styled for JSON editing with monospace font."""
class Media:
css = {'all': ('css/json-editor.css',)}
js = ('js/json-editor.js',)
# Custom panel class
from wagtail.admin.panels import FieldPanel
class LabeledFieldPanel(FieldPanel):
"""Field panel with an info badge next to the label."""
def __init__(self, field_name, label_badge=None, *args, **kwargs):
super().__init__(field_name, *args, **kwargs)
self.label_badge = label_badge
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context['label_badge'] = self.label_badge
return context
Use insert_editor_js to add JavaScript that enhances the editor. For example, add a character counter or auto-save indicator:
// static/js/admin-editor.js
document.addEventListener('DOMContentLoaded', function() {
// Add character count to text fields
document.querySelectorAll('textarea[maxlength]').forEach(function(el) {
var counter = document.createElement('small');
counter.className = 'char-counter';
el.parentNode.appendChild(counter);
function update() {
var remain = el.maxLength - el.value.length;
counter.textContent = remain + ' characters remaining';
counter.style.color = remain < 50 ? '#dc2626' : '#6b7280';
}
el.addEventListener('input', update);
update();
});
});
4.8 Custom Favicon and Header Branding
Wagtail uses a default Wagtail favicon. Replace it by overriding the favicon block in the base admin template:
{% extends 'wagtailadmin/base.html' %}
{% load static %}
{% block branding_favicon %}
<link rel="shortcut icon" href="{% static 'images/favicon.ico' %}" />
{% endblock %}
{% block branding_logo %}
<img src="{% static 'images/brand-logo.svg' %}" alt="Brand"
style="height: 32px;">
{% endblock %}
Place this at templates/wagtailadmin/base.html. The branding_favicon block replaces the favicon link tag, and branding_logo replaces the logo in the admin header.
5. Common Errors & Solutions
Error: "Module 'wagtail.hooks' has no attribute 'register'"
You are importing from wagtail import hooks but using @hooks.register(). The decorator is @hooks.register() (the import is fine). Ensure the parentheses are present: @hooks.register('insert_global_admin_css'), not @hooks.register.
Error: Custom CSS not appearing in admin
Most common cause: you forgot to run python manage.py collectstatic after adding or modifying static files. Second cause: the static file path in @hooks.register('insert_global_admin_css') is relative to the static root — ensure the file exists in the correct app's static/ directory. Third: cache — hard-refresh (Ctrl+Shift+R) or restart the dev server.
Error: ModelAdmin not showing in sidebar
Ensure you called modeladmin_register(YourAdminClass) (not just defined the class) AND the file is named wagtail_hooks.py. Wagtail only auto-discovers hooks in files named wagtail_hooks.py. If you use a different filename, import it in apps.py ready() method.
Error: Custom admin view returns 403 or redirects to login
Custom views are not automatically protected by Wagtail's admin middleware. Add @login_required or @permission_required('wagtailadmin.access_admin') decorators. For Wagtail 5+, also ensure the URL pattern is mounted inside Wagtail's admin URL namespace.
Error: TemplateOverride not picked up — still seeing default Wagtail templates
Wagtail admin templates live under the wagtailadmin template namespace. Your override must have the exact same path. Verify: run python -c "import wagtail; print(wagtail.__file__)" to find the wagtail package, then check wagtail/admin/templates/wagtailadmin/login.html — your override path must match exactly, starting from wagtailadmin/ (no extra prefix). Also ensure your TEMPLATES.DIRS setting includes your project's templates directory.
6. Summary Checklist
- Created a
wagtail_hooks.pyfile in your Django app for all admin customizations - Registered custom CSS via
insert_global_admin_cssto brand the admin color scheme and logo - Overrode
wagtailadmin/login.htmlto replace the Wagtail login with branded login page - Overrode
wagtailadmin/base.htmlto replace the favicon and header logo - Registered models via
ModelAdminwith list display, filter, search, and export - Built custom admin views using hooks and registered them in the sidebar menu
- Added dashboard panels to the admin home page using
construct_homepage_panels - Injected custom JavaScript via
insert_editor_jsto enhance the editor experience - Customized the admin sidebar menu with
construct_main_menufor role-based visibility - Applied custom typography and fonts using CSS custom properties
- Tested all customizations by logging in as different user roles to verify permission gating
7. Practice Exercise
Challenge: Build a Branded Admin Dashboard for a Client CMS
Your client runs a Wagtail-powered publishing platform. They want the admin to feel like their own product. Complete the following:
- Create a
wagtail_hooks.pythat injects a custom CSS file changing the primary color to the client's brand color (#0f766e— teal) and the header background to#0f172a - Override the login page template to show a teal gradient background with a white centered card containing the client logo (
client-logo.svg) - Register an
Articlemodel viaModelAdminwith: list display (title, author, published_date, status), filter by status and author, and search by title - Add a custom dashboard panel showing: total articles, total authors, and articles published this week
- Create a custom menu item called "Reports" linking to a view that shows basic content analytics (counts by category)
- Override the favicon to
client-favicon.ico
Bonus: Add a custom editor JS that inserts a "Last auto-saved: X seconds ago" indicator in the page editor, updating every 15 seconds via polling.
When you finish, the admin should feel like a completely different CMS — branded, tailored, and professional — without a single change to Wagtail's core code.
8. Next Steps
You have learned to customize the Wagtail admin interface — injecting CSS and JavaScript, overriding templates, building custom views with ModelAdmin and hooks, and tailoring the editor experience. Your CMS now reflects your brand, not just Wagtail's defaults.
Up next: Wagtail Testing & CI/CD — Learn to write automated tests for your Wagtail projects — page model tests, API endpoint tests, streamfield validation, and image renditions. You will also set up a CI/CD pipeline with GitHub Actions to run tests on every push and deploy to staging automatically.
Further resources:
- Wagtail Hooks Reference — official documentation for all admin hooks
- ModelAdmin Reference — full ModelAdmin API with custom views
- Customising the Wagtail Admin — official guide
- Panels Reference — all panel types and configuration options
Common pitfalls to avoid:
- Don't modify Wagtail's core templates directly — always override them in your project templates directory; core changes are lost on
pip install --upgrade wagtail - Don't forget to run
collectstaticafter adding admin CSS/JS — Wagtail serves admin assets from the static root, not your app's static directory - Don't skip permission checks on custom admin views — unprotected views are accessible to anyone who knows the URL
- Avoid heavy JS dependencies in
insert_editor_js— editor scripts run on every page load and affect the editing experience; keep them lightweight
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment