Custom Themes
You can restyle the admin through theme settings, custom templates, and static files. DefaultTheme controls the default look by writing data attributes onto the <html> tag from a TablerSettings object. For deeper changes, subclass BaseTheme to bundle your own templates, static assets, and icon sets, or pass your own template and static directories to Admin.
Applying a theme
Use TablerSettings to set your color palette, border radius, and color mode. Pass it to DefaultTheme, then pass that to the theme parameter of your Admin instance.
from myapp.models import Post
from sqlalchemy import create_engine
from starlette_admin.theme import DefaultTheme, TablerSettings
from starlette_admin.contrib.sqla import Admin, ModelView
engine = create_engine("sqlite:///admin.sqlite")
admin = Admin(
engine,
title="My Admin",
theme=DefaultTheme(
settings=TablerSettings(base="slate", primary="blue", radius=2, mode="dark")
),
)
admin.add_view(ModelView(Post))
See examples/08-themes for a runnable app that picks a random theme on every startup.
This configuration applies data-bs-theme* attributes directly to the root <html> element:
<html data-bs-theme="dark"
data-bs-theme-base="slate"
data-bs-theme-primary="blue"
data-bs-theme-radius="2">
TablerSettings reference
| Attribute | Type | Default | Valid values |
|---|---|---|---|
mode |
str |
"light" |
"light", "dark" |
base |
str | None |
"stone" |
"slate", "gray", "zinc", "neutral", "stone", "pink" |
primary |
str | None |
"blue" |
"blue", "azure", "indigo", "purple", "pink", "red", "orange", "yellow", "lime", "green", "teal", "cyan", "inverted" |
radius |
float | None |
1 |
0, 0.5, 1, 1.5, 2 |
Restyling components with a class map
Core templates don't hardcode component styling. They render class attributes through the cls('role.name') Jinja helper, which resolves a semantic role such as form.save_button or list.table to a CSS class string. The default value for every role lives in starlette_admin.theme.CoreClasses.
To restyle a role, write a ClassMap subclass. Any role you don't map falls back to CoreClasses, so partial overrides are safe. Keep in mind that a button role sets the element's entire class attribute, including variant, size, and spacing, so mapping one replaces the button's appearance outright.
Class maps don't require a full custom theme. To adjust the default theme, subclass DefaultTheme and return your map from get_class_map():
from starlette_admin.theme import ClassMap, DefaultTheme
class MyClasses(ClassMap):
classes = {
# Rounded success save button instead of the default primary one
"form.save_button": "btn btn-success rounded-pill",
# Outline create button on the list toolbar
"list.create_button": "btn btn-outline-primary ms-2",
# Pill-shaped filter chips
"filter.chip": "badge rounded-pill bg-primary-subtle",
}
class MyTheme(DefaultTheme):
def get_class_map(self) -> ClassMap:
return MyClasses()
admin = Admin(engine, title="My Admin", theme=MyTheme())
Read CoreClasses.classes in starlette_admin/theme.py for the full vocabulary of roles. Roles cover three kinds of styling:
- Buttons: One role per button location, covering form footers, list toolbars, filter bars, action modals, and inline editing. The value you set becomes the button's entire class attribute.
- Component classes: Framework-specific classes a different CSS framework has to swap out, such as
list.table,modal.base, orfilter.chip. - Runtime classes: Classes the core JavaScript applies dynamically, such as
alert.successorimport.status_badge.
Building and sharing custom themes
You can package a theme and publish it on PyPI, much like a plugin. Subclass BaseTheme to build a reusable Python package that replaces the admin layout and styling across several projects, or to share a visual system with other people.
Scaffolding with Cookiecutter
Start from the official cookiecutter template. It generates a publishable package with the right directory structure and configuration files.
Install cookiecutter with your package manager. See the official installation guide for the details:
Then run the template from any directory:
The template prompts you for the theme name, package slug, version, and a few other variables. When it finishes, you have a self-contained package with:
- A
src/directory holding the theme class, icon set, and class map. - Preconfigured
templates/,static/, and translation folders. - A test suite and a runnable example application.
BaseTheme architecture
A theme sits at the root of the rendering chain, and each Admin instance has exactly one active theme. A BaseTheme subclass configures these pieces:
- Templates: Replacement templates in the package's
templates/folder, using bare relative paths such asbase.html,layout.html, orlist.html. The active theme sits above plugins in Jinja's loader chain, so it can restyle both core and plugin templates. - Static assets: Stylesheets, scripts, and images in the package's
static/directory. - Icon set: A custom
IconSetsubclass returned fromget_icon_set(), mapping semantic keys such aslist.neworauth.logoutto CSS classes. - Class map: A
ClassMapsubclass returned fromget_class_map(), as described in Restyling components with a class map. - Template globals: Global variables exposed to Jinja by overriding
template_globals().
Example theme package
from typing import Any
from starlette_admin.theme import BaseTheme, ClassMap, IconSet
class CustomIconSet(IconSet):
icons = {
"list.new": "hi hi-plus",
"default_actions.view": "hi hi-eye",
# Map remaining semantic icon keys
}
class CorporateClasses(ClassMap):
classes = {
"form.save_button": "btn btn-corporate",
# Map remaining roles to restyle; unmapped roles keep core defaults
}
class CorporateTheme(BaseTheme):
name = "corporate"
package = "corporate_theme_package" # Auto-detected from class module if omitted
def get_icon_set(self) -> IconSet:
return CustomIconSet()
def get_class_map(self) -> ClassMap:
return CorporateClasses()
def template_globals(self) -> dict[str, Any]:
return {"company_name": "Acme Corp"}
Template loader hierarchy
The template engine resolves files in this order:
- Your
templates_dir, which overrides everything below it. - The active theme's
templates/, which restyles core and plugin templates. - The namespaced plugin
templates/. - The core
starlette_admindefault templates.
To extend a theme template from a user override or a theme subclass, use the @theme Jinja prefix, for example {% extends "@theme/layout.html" %}.
Custom templates directory
To override the default HTML without building a full theme, pass a directory path to templates_dir.
Any file you put in that directory shadows the built-in template at the same relative path, and the rest of the built-in tree keeps rendering as before. For the full list of overridable templates, see Templates.
Custom static directory
To add your own CSS, JavaScript, or images without building a full theme, pass a directory path to static_dir.
Files in this directory are served alongside the built-in assets under /admin/static/. A file at my_static/custom.css, for example, becomes available at /admin/static/custom.css.
Reference the stylesheet from your templates like this:
What's next
- Templates: Override a single page, cell, or widget without forking the entire template tree.
- Extension Points: Explore hooks and customization points beyond basic themes.
- Quickstart: Build a working admin interface from scratch.