brushup: Theme-Aware Dynamic Color Palette for Emacs
Table of Contents
1. About emacs theming colorPalette
Figure 1: JPEG produced with DALL-E 4o
Emacs theming has a dirty secret: most face customizations are written against one theme and silently break against every other. You pick colors that look right in your dark theme, switch to something light for a change of scenery, and suddenly half your UI is unreadable. brushup generates a dynamic color palette from your current theme's foreground and background, so your face customizations work everywhere.
2. The Problem: Theme-Dependent Styling theming problem
Say you've customized org-mode headings, your modeline, or some niche package's faces. You hardcode hex values because they look good in your current theme:
(set-face-attribute 'org-level-1 nil :foreground "#a0b0c0") (set-face-attribute 'my-custom-face nil :background "#1a1a2e")
These colors assume a dark background. Switch to a light theme and #1a1a2e is nearly invisible against a white background, while #a0b0c0 loses all its contrast. The usual workaround is to write conditional logic:
(if (eq (frame-parameter nil 'background-mode) 'dark)
(set-face-attribute 'org-level-1 nil :foreground "#a0b0c0")
(set-face-attribute 'org-level-1 nil :foreground "#3a4a5a"))
This doubles your configuration for every face you touch and still only handles two themes. If you use three or four themes across different times of day or contexts, the branching gets unwieldy fast. What you actually want is a way to say "give me a color that's 30% of the way from the background toward the foreground" and have that resolve correctly regardless of the theme.
3. How brushup Works emacs architecture
brushup reads two colors from your current theme: the default foreground and the default background. From these, it generates a parametric gradient, a series of intermediate colors stepping from one toward the other. The result is twelve palette variables: six foreground gradients (brushup-fg-1 through brushup-fg-6, stepping from the foreground toward the background) and six background gradients (brushup-bg-1 through brushup-bg-6, stepping from the background toward the foreground).
You describe your color intent in relative terms, and the palette resolves it against whatever theme is active.
The gradient step size is controlled by brushup-gradient-step (default: 7%). Each level moves that percentage further along the lightness axis. brushup-fg-1 is a subtle shift, barely distinguishable from the raw foreground. brushup-fg-6 is a dramatic shift, nearly halfway to the background.
brushup also detects whether your theme is dark or light using a luminosity calculation on the background color, exposing the result as brushup-dark-p. This means you never need to check background-mode yourself.
When brushup-mode is enabled, the package hooks into enable-theme-functions. Every time you load or switch themes, brushup re-generates the entire palette and re-evaluates all registered styles. You describe your color intent in relative terms, and the palette resolves it against whatever theme is active.
4. The Palette theming reference
Here's the full set of palette variables:
| Variable | Description |
|---|---|
brushup-fg |
Raw theme foreground |
brushup-bg |
Raw theme background |
brushup-fg-1 to brushup-fg-6 |
Foreground gradient (1=subtle, 6=strong shift toward bg) |
brushup-bg-1 to brushup-bg-6 |
Background gradient (1=subtle, 6=strong shift toward fg) |
brushup-bg-1_0 |
Very subtle background shift (solaire-like effect) |
brushup-dark-p |
t if current theme is dark |
The gradient levels map naturally to common styling needs:
- Level 1-2: Subtle emphasis. Comments, inactive modeline text, de-emphasized UI elements.
- Level 3-4: Moderate contrast. Secondary headings, sidebar text, annotations.
- Level 5-6: Strong contrast. Borders, separators, high-visibility indicators.
5. Demo: Switching Themes emacs demonstration
Here's what it looks like when you switch themes with brushup active. Every registered face customization re-evaluates instantly.
6. The Style System emacs configuration
brushup doesn't apply colors directly. Instead, you register forms in brushup-styles, a list of expressions that get evaluated whenever the palette updates. Each form can reference any palette variable:
(add-to-list 'brushup-styles
'(set-face-attribute 'org-level-1 nil :foreground brushup-fg-3))
(add-to-list 'brushup-styles
'(set-face-attribute 'line-number nil
:foreground brushup-fg-5
:background brushup-bg-1))
When the theme changes, brushup regenerates the palette values, then walks the list and evaluates each form. Errors in individual forms are caught and reported without stopping the rest, so a broken customization won't take down your entire UI.
6.1. use-package Integration
brushup registers a custom :brushup keyword with use-package, so you can co-locate your style registrations with the packages they customize:
(use-package magit
:ensure t
:brushup
(add-to-list 'brushup-styles
'(set-face-attribute 'magit-section-heading nil
:foreground brushup-fg-2)))
The :brushup keyword defers evaluation until the package loads, then registers the forms. This keeps theme-dependent styling next to the package configuration it belongs with.
7. Getting Started emacs installation
brushup is on GitHub. Install with elpaca, straight.el, or manually, then enable brushup-mode.
(use-package brushup :ensure (:host github :repo "chiply/brushup") :config (brushup-mode 1))
If you're already maintaining a pile of theme-conditional face settings, brushup is a good excuse to delete them. Replace the hardcoded hex values with palette variables, register them as styles, and let the gradient do the work.