DEV Community

Cover image for Baton: a cool, modern and responsive Django admin application based on Bootstrap 5

Posted on • Updated on

Baton: a cool, modern and responsive Django admin application based on Bootstrap 5

I must admit, one of the features that made me immediately fall in love with Django was the built-in admin application. Coming from a PHP framework, constructing all the CRUD stuff by hand, application after application, and suddenly having all this stuff autogenerated was a big deal.

The problem is, the Django default admin application doesn't look really cool, does it? It surely does its job and includes the most important features, but I'm not sure it could be delivered to a client without tweaking it. I mean, almost every web application framework or CMS has a pretty admin area, and people are somewhat used to something similar.

Here at Otto srl we (also) develop web applications, and we do that from scratch using the Django framework. We tend to use the Django admin application to provide access to data whenever possible.
We used to adopt django-suit as admin application because it really looked nice. The problem is that django-suit was based on bootstrap 2 and even if darklow started developing a v2 based on bootstrap 4, such version never really come to life.

That's why we decided to develop our own Django admin application, and that's the beginning of Django Baton. The project started in February 2017, so it turned 4 years old this year :).
We use it extensively in every web application, we really have many projects deployed in production using it and that's why we continue its development and fix bugs quite quickly.

Django Baton

Baton is a cool, modern and responsive Django admin application based on Bootstrap 5.

It was built with one concept in mind: override as few Django stuff as possible.

If you're wondering why the answer is: in order to stay as much compatible with the default Django admin.
We are quite satisfied with the default admin app features, we just want it to be more appealing. Not overriding things is also a good way to prevent bugs. Baton can work with every Django/python version without too much effort, and only recently, in v2, we dropped support for Django < 2.1, and it was by design, because boys, it's time to abandon python 2 and move on.

First things first

  • You can try it. There's a live demo available here.
  • It's MIT licensed and the repo project is available here.
  • The documentation covers every aspect of the application, and it's available here.


  • Based on Bootstrap 5 and FontAwesome Free 5
  • Fully responsive (really)
  • Custom and flexible sidebar menu
  • Configurable search field
  • Text input filters and dropdown list filters facilities
  • Form tabs out of the box
  • Easy way to include templates in the change form and change list pages
  • Easy way to add attributes to change list table rows/cells
  • Collapsable stacked inline entries
  • Lazy loading of uploaded images
  • Optional display of changelist filters in a modal
  • Optional use of changelist filters as a form (combine some filters at once and perform the search action)
  • Optional index page filled with google analytics widgets
  • Customization available recompiling the js app provided
  • Supports custom translations

Getting started

You can install Baton directly from pip:

$ pip install django-baton
Enter fullscreen mode Exit fullscreen mode

Add baton and baton.autodiscover to your INSTALLED_APPS:

    # ...
    'baton', # place it before django.contrib.admin
    # ...
    'baton.autodiscover', # place it at the very end
Enter fullscreen mode Exit fullscreen mode

Replace django.contrib.admin in your project urls, and add Baton urls:

# from django.contrib import admin
from baton.autodiscover import admin
from django.urls import path, include

urlpatterns = [
    path('baton/', include('baton.urls')),
Enter fullscreen mode Exit fullscreen mode

You need to add both baton and baton.autodiscover to your INSTALLED_APPS in order to avoid registering all applications directly to Baton. Baton autodiscover module will run last and will detect all the applications registered to the default admin and make use of them. This way you can easily switch from one admin application to another one, and you can easily integrate Baton into any existing project.


Let's see a full configuration example:

# in your
    'SITE_HEADER': 'Baton',
    'SITE_TITLE': 'Baton',
    'INDEX_TITLE': 'Site administration',
    'SUPPORT_HREF': '',
    'COPYRIGHT': 'copyright © 2020 <a href="">Otto srl</a>', # noqa
    'POWERED_BY': '<a href="">Otto srl</a>',
    'MENU_TITLE': 'Menu',
    'GRAVATAR_DEFAULT_IMG': 'retro',
    'LOGIN_SPLASH': '/static/core/img/login-splash.png',
        'label': 'Search contents...',
        'url': '/search/',
    'MENU': (
        { 'type': 'title', 'label': 'main', 'apps': ('auth', ) },
            'type': 'app',
            'name': 'auth',
            'label': 'Authentication',
            'icon': 'fa fa-lock',
            'models': (
                    'name': 'user',
                    'label': 'Users'
                    'name': 'group',
                    'label': 'Groups'
        { 'type': 'title', 'label': 'Contents', 'apps': ('flatpages', ) },
        { 'type': 'model', 'label': 'Pages', 'name': 'flatpage', 'app': 'flatpages' },
        { 'type': 'free', 'label': 'Custom Link', 'url': '', 'perms': ('flatpages.add_flatpage', 'auth.change_user') },
        { 'type': 'free', 'label': 'My parent voice', 'default_open': True, 'children': [
            { 'type': 'model', 'label': 'A Model', 'name': 'mymodelname', 'app': 'myapp' },
            { 'type': 'free', 'label': 'Another custom link', 'url': '' },
        ] },
    'ANALYTICS': {
        'CREDENTIALS': os.path.join(BASE_DIR, 'credentials.json'),
        'VIEW_ID': '12345678',
Enter fullscreen mode Exit fullscreen mode

I won't describe here each available setting, they're all well explained in the documentation or in the repo page.

But I'd like to explore here in more detail some of the coolest features which Baton provides.

Login splash

As shown in the cover image of this post, you can easily customize the login page providing an image background, just use the LOGIN_SPLASH option.


Screenshot from 2021-03-22 10-41-03

The sidebar is very handy. True, the last Django version comes with a default sidebar menu in its admin app but is not very configurable and I don't like it very much.
I think Baton does a good job here, providing a nice and extremely configurable sidebar menu to your application. Baton also provides a search field widget, that can be very powerful, more on it later.

Baton sidebar is visible by default on desktop devices, but you can decide to collapse it (see MENU_ALWAYS_COLLAPSED option) and make it accessible through a dedicated icon in the top bar if you need more space for your data (same behaviour as for mobile devices).
Also, the user area can be collapsed to save some space in the sidebar, see the COLLAPSABLE_USER_AREA option.

Baton menu system offers four kinds of menu items: title, app, model and free. I won't cover here all the possible configuration since this is explained in detail in the documentation, but let's say that:

  • title items can be used to group other items
  • app items lets you create a menu iten for an application, whose children items are all the models of the app (but you can customize the children prop)
  • model items lets you create a menu item for a model
  • free items can be used to create any other kind of menu items (you can provide an URL, an icon and a regular expression to indicate when an item should be considered active, the match is calculated against the URL path).

What follows is an example of the menu configuration.

help_modal = '''
modalContent = `
        <p><strong style="font-weight: 900">Welcome to django-baton!</strong></p>
modal = new Baton.Modal({
    title: 'Help',
    content: modalContent,

    'MENU': (
        { 'type': 'title', 'label': 'System', 'apps': ('auth', ) },
            'type': 'model',
            'app': 'auth',
            'name': 'user',
            'label': 'Users',
            'icon': 'fa fa-user',
            'type': 'model',
            'app': 'auth',
            'name': 'group',
            'label': 'Groups',
            'icon': 'fa fa-users',
        { 'type': 'title', 'label': 'Contents', 'apps': ('kitchensink', ) },
            'type': 'model',
            'app': 'kitchensink',
            'name': 'news',
            'label': 'News',
            'icon': 'fa fa-newspaper',
        { 'type': 'title', 'label': 'Utilitites', 'apps': () },
            'type': 'free',
            'url': 'javascript:%s' % help_modal,
            'label': 'Help',
            'icon': 'fa fa-life-ring',
            'type': 'free',
            'url': '',
            'label': 'Otto',
            'icon': 'fa fa-grin-stars',

Enter fullscreen mode Exit fullscreen mode

As you can see, you can use title items to group menu items and you can use the app keyword to set the group visibility so that users which cannot act on children will not see the group entirely.

You can use free voices to add custom URLs or even to trigger some javascript function.

The search field

The last Baton release added support for a full-text search component in the sidebar. Baton provides the component and the autocomplete feature (suggest as you type), while you should provide your backend implementation. This way you can decide which contents to filter and which results to provide given a search text.

Screenshot from 2021-03-22 12-04-49

You need to configure it this way:

    'label': 'Label shown as placeholder',
    'url': '/api/path/',
Enter fullscreen mode Exit fullscreen mode

The label will be shown as a placeholder for the input field, so you can set it to something like "search blog and news...", while the url parameter is the endpoint for your backend implementation.

The backend implementation should return the search result in a format that Baton can understand:

    "length": 2,
    "data": [
        { "label": "My result #1", icon: "fa fa-edit", "url": "/admin/myapp/mymodel/1/change" },
        // ...
Enter fullscreen mode Exit fullscreen mode

That's simply a JSON. You should provide a length attribute that represents the number of results and an array of results collected in the data attribute. Each result should be an object containing a label and an url properties. Optionally you can also define an icon property.

While the user types some chars in the fieldset, Baton will call your endpoint passing the inserted text and asking for results. Results are then presented to the user. A click on a result will cause the browser to follow the associated URL. The user can move between results also using the keyboard arrows and select a result hitting the return key.

Let's see a backend implementation example:

def admin_search(request):
    text = request.GET.get('text', None)
    res = []
    news = News.objects.all()
    if text:
        news = news.filter(title__icontains=text)
    for n in news:
            'label': str(n) + ' edit',
            'url': '/admin/news/news/%d/change' %,
            'icon': 'fa fa-edit',
    if text.lower() in 'Lucio Dalla Wikipedia'.lower():
            'label': 'Lucio Dalla Wikipedia',
            'url': '',
            'icon': 'fab fa-wikipedia-w'
    return JsonResponse({
        'length': len(res),
        'data': res
Enter fullscreen mode Exit fullscreen mode

In this case, we're searching on our News model, adding also the possibility to search for a wiki page. As you can see you're not restricted to display results tied to your models, you can literally send the user everywhere!

Jacascript signals

Baton tries to avoid overriding templates and views. All it's styled through CSS and when more control is needed, javascript is used in order to adjust the DOM. That means that Baton performs DOM manipulation at startup, and there may be situations in which you need to know when it finishes its job.

For this reason, Baton provides an easy way to attach listeners that will be notified when some event occurs:

  • onNavbarReady: dispatched when the navbar is fully rendered
  • onMenuReady: dispatched when the menu is fully rendered (probably the last event fired since the menu contents are retrieved async)
  • onTabsReady: dispatched when the change form tabs are fully rendered
  • onMenuError: dispatched if the request sent to retrieve menu contents fails
  • onReady: dispatched when Baton JS has finished its sync job

You can then attach your listener this way:

Baton.Dispatcher.register('onMenuReady', function () { console.log('BATON MENU IS READY') })
Enter fullscreen mode Exit fullscreen mode

Where to put this code?

You need to override the admin/base_site.html template. This is something I always do.


Just define an app before baton in the INSTALLED_APPS, let's call it core for example. Create a template core/templates/admin/base_site.html and just copy the code of the baton/templates/admin/base_site.html. Then you can add your own stuff.

Other javascript utilities

Baton uses javascript a lot to provide its features. So it also provides some useful classes in its namespace:

  • Dispatcher: a mediator pattern dispatcher you can use to emit and subscribe to events.
  • Modal: a modal class that lets you use bootstrap modals without dealing with all the markup.

You can read more about them and see some examples in the documentation.


Baton does not override Django stuff, you already know that. So it almost doesn't define new strings. Almost, because some strings are needed and used here and there and cannot be retrieved from the page content. Baton comes with en and it translations out of the box, but you can add your locale translations, and even change the default strings simply by overriding Baton translation property:

Baton.translations = {
  unsavedChangesAlert: 'You have some unsaved changes.',
  uploading: 'Uploading...',
  filter: 'Filter',
  close: 'Close',
  save: 'Save',
  search: 'Search',
  cannotCopyToClipboardMessage: 'Cannot copy to clipboard, please do it manually: Ctrl+C, Enter',
  retrieveDataError: 'There was an error retrieving the data'
Enter fullscreen mode Exit fullscreen mode

Since you'll put this code inside a template you'll be able to use every i18n template tag.

Include, include and yes, include

This feature was added to please earlier Baton users. Its a feature coming from django-suit, something that I've never used so much, but many developers did.

Baton uses the template HTML tag to inject contents inside the document. All this functionalities are well documented so I'll simply link the documentation page for each of them:

And then, there is one more cool "include" functionality, that lets you inject attributes in the change list table rows. This is nice, because you can inject CSS classes (.table-info, .table-success) and attributes (title will show its content on a tooltip on mouseover). And most important, you can inject this stuff inside a table row (tr), a single table cell (td) or even some other custom element making use of the selector and getParent properties. All this is well documented.

Form tabs

Screenshot from 2021-03-22 12-39-46

If you used django-suit you loved its tab system. Forms become just too much cleaner using them that I just cannot think any more of a change form without this feature. For this reason, Baton had support for form tabs since its first days. And it brings tab support even further, allowing to group fieldsets and inlines, and to change tabs ordering.

Everything is done just by adding some well-defined CSS classes to your fieldsets.

Take a look at the documentation for a complete explanation of all the rules. Keep in mind that you can also open a tab by default just by appending the right hash to an URL.

Change list filters

A few words about the change list filters. They're cool but not always perfect in the default admin. I mean, sometimes the FK filtering is a mess because you have too many entries. Sometimes you need to perform a text search. Sometimes there are so many filters you may desire to have them in a different place.

Baton adds some juice to your admin filters:

  • Input text filters: add simple text filters (something like a search_fields but restricted to one field).
  • Dropdown filters: when the related entries grow too much, just avoid having an infinite unordered list and use dropdowns.
  • Filters in modal: I like this one: open your list filters in a modal above the document. It's clean.
  • Filters in modal: I like this one: open your list filters on a modal above the document. It's clean.
  • Filters form: by default, you can apply one filter at a time. That can be cumbersome, and there are times when you prefer to tweak some filters at once and then perform the search action. Using this option Baton will transform your list filters in a form with a submit button: adjust all your filters and submit them all together.

And more...

There are many more features that Baton provides:

Take your time to surf through the documentation, you'll find everything explained.


This has been a long journey, I'd like just to spend a few more words about Baton customization.

Baton comes with an arguably neutral palette, but there are situations in which even the administrative area should be heavily customized in order to reflect the brand, and changing the logo is not enough.

We've seen that Baton does all its job through styling and javascript. That's true, and in fact, Baton core it's a modern js application injected in the base_site.html admin template.

Baton uses webpack to build the javascript application which also includes all the CSS. For this reason, it's very simple to customize how it looks. The Baton core app is nothing more that a static served content that obeys to the same rules of all other static resources: the first installed app which serves that content wins.

That means that you can serve a customized version of the baton core app from an application defined before baton in your installed apps.

But how can you create a customized version of Baton core app?


First: clone the Baton repository:

$ git clone
Enter fullscreen mode Exit fullscreen mode

Cd into the core app folder and install all dependencies:

$ cd django-baton/baton/static/baton/app/
$ npm install
Enter fullscreen mode Exit fullscreen mode

Now you can make your modifications, in particular, you may want to change the font, the colours, or any bootstrap configurable variable. That's easy, just edit the src/styles/_variables.scss file. You can override any bootstrap variable in place!

Of course, you can see your changes live: Baton comes with a webpack dev server ready to be started. It's necessary to change the path of the included Baton app in the base_site.html template. The default Baton template already have the right path commented out, you just need to switch the two script tags:

<!-- <script src="{% static 'baton/app/dist/baton.min.js' %}"></script> comment the compiled src and uncomment the webpack served src -->
<script src="http://localhost:8080/dist/baton.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

Now you can see the changes live, and after editing and saving a source file the page refreshes.

When you're satisfied with your job you can compile your app:

npm run compile
Enter fullscreen mode Exit fullscreen mode

And finally you can copy the generated build in your custom app defined before baton (remember also to switch again the script tags in your base_site.html template):

cp dist/baton.min.js ROOTAPP/static/baton/app/dist/
Enter fullscreen mode Exit fullscreen mode

That's it!


Baton is a Django application that aims to provide a better admin experience without changing too much stuff.

I would say that using Baton you're still using Django admin application, it feels Django, but at the same time it has a cooler face and some helpful additions which can help you design a great UI/UX.

The project is completely open-source, each contribution is welcome and if you like it, please ★ star it on GitHub!

Top comments (2)

llanilek profile image
Neil Hickman

I think the idea has merit. There have been many a Django admin that has come and gone in the past. However, Baton doesn't seem that aesthetically pleasing as compared to the standard admin. I think if the look and feel were worked on it would be much more appealing.

kearabiloe profile image
Kearabiloe Ledwaba

hmm..i agree with you..ultimately I went with it because it is well designed & documented, beats starting from "scratch"