Creating Emacs Color Themes, Revisited
Creating Emacs color themes is a topic I hadn’t thought much about in recent years. My first theme (Zenburn) has been in maintenance mode for ages, and Solarized mostly runs itself at this point. But working on my ports of Tokyo (Night) Themes and Catppuccin (Batppuccin) made me re-examine the whole topic with fresh eyes. The biggest shift I’ve noticed is that multi-variant themes (light/dark/high-contrast from a shared codebase) have become the norm rather than the exception, and that pattern naturally leads to reusable theming infrastructure.
The task has always been simultaneously easy and hard. Easy because deftheme
and custom-theme-set-faces are well-documented and do exactly what you’d
expect. Hard because the real challenge was never the mechanics – it’s knowing
which faces to theme and keeping your color choices consistent across hundreds
of them.
Note: In Emacs, a face is a named set of visual attributes – foreground color, background, bold, italic, underline, etc. – that controls how a piece of text looks. Themes work by setting faces to match a color palette. See also the Elisp manual’s section on custom themes for the full API.
The Classic Approach
The traditional way to create an Emacs theme is to write a deftheme form,
then set faces one by one with custom-theme-set-faces:
(deftheme my-cool-theme "A cool theme.")
(custom-theme-set-faces
'my-cool-theme
;; The `t` means "all display types" -- you can also specify different
;; colors for different displays (GUI vs 256-color terminal, etc.)
'(default ((t (:foreground "#c0caf5" :background "#1a1b26"))))
'(font-lock-keyword-face ((t (:foreground "#bb9af7"))))
'(font-lock-string-face ((t (:foreground "#9ece6a"))))
;; ... 200+ more faces
)
(provide-theme 'my-cool-theme)
This works fine and gives you total control. Many excellent themes are built exactly this way. In practice, a lot of new themes start their life as copies of existing themes – mostly to avoid the leg-work of discovering which faces to define. You grab a well-maintained theme, swap the colors, and you’re halfway there.
That said, the approach has a couple of pain points:
- You need to know what faces exist. Emacs has dozens of built-in faces, and
every popular package adds its own. Miss a few and your theme looks polished
in some buffers but broken in others.
list-faces-displayis your friend here, but it only shows faces that are currently loaded. - Consistency is on you. With hundreds of face definitions, it’s easy to use slightly different shades for things that should look the same, or to pick a color that clashes with your palette. Nothing enforces coherence – you have to do that yourself.
- Maintaining multiple variants is tedious. Want a light and dark version? You’re duplicating most of the face definitions with different colors.1
One more gotcha: some packages use variables instead of faces for their colors
(e.g., hl-todo-keyword-faces, ansi-color-names-vector). You can set those
with custom-theme-set-variables, but you have to know they exist first. It’s
easy to think you’ve themed everything via faces and then discover a package
that hard-codes colors in a defcustom.
How big of a problem the face tracking is depends on your scope. If you only
care about built-in Emacs faces, it’s pretty manageable – that’s what most of
the bundled themes do (check wombat, deeper-blue, or tango – they
define faces almost exclusively for packages that ship with Emacs and don’t
touch third-party packages at all). But if you want your theme to look good in
magit, corfu, vertico, transient, and a dozen other popular packages,
you’re signing up for ongoing maintenance. A new version of magit adds a face
and suddenly your theme has gaps you didn’t know about.
I still do things this way for Tokyo Themes and Batppuccin, but the more themes I maintain the more I wonder if that’s overkill.
Every Multi-Variant Theme Is a Mini Framework
Here’s something worth pointing out: any theme that ships multiple variants is already a framework of sorts, whether it calls itself one or not. The moment you factor out the palette from the face definitions so that multiple variants can share the same code, you’ve built the core of a theming engine.
Take Tokyo Themes as an example. There are four variants (night, storm, moon, day), but the face
definitions live in a single shared file (tokyo-themes.el). Each variant is a
thin wrapper – just a deftheme, a palette alist, and a call to the shared
tokyo--apply-theme function:
(require 'tokyo-themes)
(deftheme tokyo-night "A clean dark theme inspired by Tokyo city lights.")
(tokyo--apply-theme 'tokyo-night tokyo-night-colors-alist)
(provide-theme 'tokyo-night)
That’s the entire theme file. The palette is defined elsewhere, and the face
logic is shared – which is exactly how you solve the variant duplication problem
mentioned earlier. In theory, anyone could define a new palette alist and call
tokyo--apply-theme to create a fifth variant. The infrastructure is already
there – it’s just not explicitly marketed as a “framework.”
This is exactly how the theming features of packages like Solarized and Modus evolved. They started as regular themes, grew variants, factored out the shared code, and eventually exposed that machinery to users.
Meta-Themes
Some theme packages went a step further and turned their internal infrastructure into an explicit theming API.
Solarized
solarized-emacs started as a
straight port of Ethan Schoonover’s Solarized palette, but over time it grew
the ability to create entirely new themes from custom palettes. You can use
solarized-create-theme-file-with-palette to generate a new theme by supplying
just 10 colors (2 base + 8 accent) – it derives all the intermediate shades
and maps them to the full set of faces:
(solarized-create-theme-file-with-palette 'dark 'my-solarized-dark
'("#002b36" "#fdf6e3" ;; base colors
"#b58900" "#cb4b16" "#dc322f" "#d33682" ;; accents
"#6c71c4" "#268bd2" "#2aa198" "#859900"))
This is how Solarized’s own variants (dark, light, gruvbox, zenburn, etc.) are
built internally. I’ll admit, though, that I always found it a bit weird to ship
themes like Gruvbox and Zenburn under the Solarized umbrella. If you install
solarized-emacs and find a solarized-gruvbox-dark theme in the list, the
natural reaction is “wait, what does Gruvbox have to do with Solarized?” The
answer is “nothing, really – they just share the theming engine.” That makes
perfect sense once you understand the architecture, but I think it’s confusing
for newcomers. It was part of the reason I was never super excited about this
direction for solarized-emacs.
Modus Themes
The modus-themes take a different approach. Rather than generating new theme files, they offer deep runtime customization through palette overrides:
(setq modus-themes-common-palette-overrides
'((bg-main "#1a1b26")
(fg-main "#c0caf5")
(keyword magenta-warmer)))
You can override any named color in the palette without touching the theme
source. The result feels like a different theme, but it’s still Modus under the
hood with all its accessibility guarantees. The overrides apply to whichever
Modus variant you load, and modus-themes-toggle switches between variants
while keeping your overrides intact. Protesilaos’s
ef-themes share the same
architecture.
Theming Frameworks
If you want to create something brand new rather than customize an existing theme family, there are a couple of frameworks designed for this.
Autothemer
autothemer provides a macro that
replaces the verbose custom-theme-set-faces boilerplate with a cleaner,
palette-driven approach:
(autothemer-deftheme
my-theme "A theme using autothemer."
;; Display classes: 24-bit GUI, 256-color terminal, 16-color terminal
((((class color) (min-colors 16777216)) ((class color) (min-colors 256)) t)
(my-bg "#1a1b26" "black" "black")
(my-fg "#c0caf5" "white" "white")
(my-red "#f7768e" "red" "red")
(my-green "#9ece6a" "green" "green"))
;; Face specs -- just reference palette names, no display class noise
((default (:foreground my-fg :background my-bg))
(font-lock-keyword-face (:foreground my-red))
(font-lock-string-face (:foreground my-green))))
You define your palette once as named colors with fallback values for different display capabilities (GUI frames and terminals support different color depths, so themes need appropriate fallbacks for each). Then you reference those names in face specs without worrying about display classes again. Autothemer also provides some nice extras like SVG palette previews and helpers for discovering unthemed faces.
Base16 / Tinted Theming
base16-emacs is part of the larger Tinted Theming ecosystem. The idea is that you define a scheme as 16 colors in a YAML file, and a builder generates themes for Emacs (and every other editor/terminal) from a shared template. You don’t write Elisp at all – you write YAML and run a build step.
This is great if you want one palette to rule all your tools, but you give up fine-grained control over individual Emacs faces. The generated themes cover a good set of faces, but they might not handle every niche package you use.
From Scratch vs. Framework: Pros and Cons
| From Scratch | Meta-Theme / Framework | |
|---|---|---|
| Control | Total – every face is yours | Constrained by what the framework exposes |
| Consistency | You enforce it manually | The framework helps (palette-driven) |
| Coverage | You add faces as you discover them | Inherited from the base theme/template |
| Maintenance | You track upstream face changes | Shifted to the meta-theme maintainers |
| Multiple variants | Duplicate or factor out yourself | Built-in support |
| Learning curve | Just deftheme |
Framework-specific API |
When to Use What
I guess relatively few people end up creating theme packages, but here’s a bit of general advice for them.
If you want total control over every face and you’re willing to put in the maintenance work, roll your own. This makes sense for themes with a strong design vision where you want to make deliberate choices about every element. It’s more work, but nothing stands between you and the result you want.
If you mostly like an existing theme but want different colors, customizing a meta-theme (Modus, Solarized, ef-themes) is a good bet. You get battle-tested face coverage for free, and the palette override approach means you can tweak things without forking. Keep in mind, though, that the face coverage problem doesn’t disappear – you’re just shifting it to the meta-theme maintainers. How comprehensive and up-to-date things stay depends entirely on how diligent they are.
If you’re creating something new but don’t want to deal with the boilerplate, use a framework. Autothemer is the best fit if you want to stay in Elisp and have fine control. Base16/Tinted Theming is the pick if you want one palette definition across all your tools.
Parting Thoughts
I’m still a “classic” – I like rolling out my themes from scratch. There’s something satisfying about hand-picking the color for every face. But I won’t pretend it doesn’t get tedious, especially when you maintain several themes across multiple variants. Every time a package adds new faces, that’s more work for me.
If I were starting fresh today, I’d seriously consider Autothemer or building on top of a meta-theme (extracted from my existing theme packages). The time you save on maintenance is time you can spend on what actually matters – making your theme look good.
On the topic of maintenance – one area where AI tools can actually help is
extracting the relevant faces from a list of packages you want to support.
Instead of loading each package, running list-faces-display, and eyeballing
what’s new, you can ask an LLM to scan the source and give you the face
definitions. It’s also handy for periodically syncing your theme against the
latest versions of those packages to catch newly added faces. Not glamorous
work, but exactly the kind of tedium that AI is good at.
That’s all I have for you today. Keep hacking!
-
Later on you’ll see that’s a pretty easy problem to address. ↩