🏆 I provide private lessons on Emacs, Linux, and Life in general: https://protesilaos.com/coach/. Lessons continue throughout the year.

Notes for aspiring Emacs theme developers

In Emacs a “theme” is a set of configurations that can be enabled or disabled as a block. Each of those controls a construct of the rendering engine known as a “face”. Faces store the properties that are associated with each element on display. These properties encompass background and/or foreground colours as well as typographic attributes, such as the font weight or slant.

Finding faces

Themes are programs written in Emacs Lisp (Elisp), whose intended role is to control faces. We can learn about all the faces that are loaded in the current session with M-x list-faces-display. The command will produce a buffer with the symbol (i.e. unique name) of the face and a preview of how it looks.

You can always consult the help page of a given symbol with C-h o (M-x describe-symbol). Place the point over a face’s symbol, type C-h o to have the thing at point as the default option. Select that (such as with M-n) to get a description of what it is supposed to do.

If we do this over the cursor face, we get the following:

Basic face for the cursor color under X. Currently, only the ‘:background’ attribute is meaningful; all other attributes are ignored. The cursor foreground color is taken from the background color of the underlying text.

Note: Other faces cannot inherit from the cursor face.

As with all *Help* buffers, the ones for individual faces contain a link to the library that defines them. We are informed, for instance, that the cursor is defined in faces.el. So we can always visit the source code from there whenever we need to understand more about the item of our inquiry.

Note that list-faces-display will only cover the libraries that are currently loaded, but not necessarily the faces that your active theme defines. If you have installed some package that you have not used yet, then any faces it may be defining will not be immediately available in the *Faces* buffer. To actually include those in the list, you need to either use their package or explicitly load the relevant file with M-x load-library. You can always regenerate the *Faces* buffer by typing g while inside of it.

Configuring an individual face

Before we proceed to write a fully fledged theme, let us first examine how to control faces one by one. The function dedicated to that task is set-face-attribute. Read its documentation string with C-h f followed by its symbol. This is important because it provides valuable information about the properties that a face may be associated with. You will need it when configuring your own theme.

Assuming you read the documentation of set-face-attribute, let us consider this example:

(set-face-attribute 'cursor nil :background "red")

We have learnt that the cursor only recognises a :background property and will ignore any other. What we do here is instruct it to use the generic red colour.

To confirm that this works, place the point to the right of the closing parenthesis and type C-x C-e (which calls eval-last-sexp). Your cursor show now be coloured red. If you were to put this in your initialisation file, or any other library that gets loaded when you open Emacs, your cursor would always get the colour you specified (unless something else overrides it later on, but you get the point).

A good use-case for this is to define your font families for the three main constructs of default, variable-pitch, and fixed-pitch.

This is the gist of what is included in the manual of the Modus themes on the topic of font configurations for Org (and others)

;; my custom build of Iosevka
;; https://gitlab.com/protesilaos/iosevka-comfy
(set-face-attribute 'default nil :font "Iosevka Comfy-15")

(set-face-attribute 'variable-pitch nil :family "Source Sans Pro" :height 1.0)

(set-face-attribute 'fixed-pitch nil :family "Iosevka Comfy" :height 1.0)

Depending on what you want to do, you can use Elisp to further control things. Here we can be a bit more succinct by using dolist (remember that C-h f, C-h v, or just C-h o are among your most valuable tools in your Emacs journey).

(dolist (face '(default fixed-pitch))
  (set-face-attribute face nil :family "Iosevka Comfy"))

Using colours

We can find the names of all generic colours with list-colors-display. Notice how earlier we specified the :background of the cursor face to a "red" value. Alternatively, one could use a hexadecimal RGB code, such as "#ff0000" for pure red. I prefer the latter because it is more precise and flexible.

How you specify colours is ultimately up to you. Picking the right values is not an easy task. It is a field of endeavour that stands at the intersection or art and science, as I explained in my essay about the design of the Modus themes.

Deconstructing an Emacs face

While set-face-attribute is perfectly fine for a few items, it becomes inefficient at scale. This is why Emacs provides the custom-theme-set-faces function. Before we start using that, we must first understand what the specifications of a face are.

Consider this excerpt from M-x find-library RET faces (here “RET” means to type the command, then confirm your choice with the Return/Enter key, and follow it up with the “faces” library).

(defface tab-bar
  '((((class color) (min-colors 88))
     :inherit variable-pitch
     :background "grey85"
     :foreground "black")
    (((class mono))
     :background "grey")
    (t
     :inverse-video t))
  "Tab bar face."
  :version "27.1"
  :group 'basic-faces)

We can read all about these specs with C-h o defface. Again, read the docs to save yourself from trouble and frustration. While you start making a habit of that, let me simplify this defface for you (extra space for didactic purposes):

(defface tab-bar
  '(

    (
     ((class color) (min-colors 88))
     :inherit variable-pitch
     :background "grey85"
     :foreground "black")

    (
     ((class mono))
     :background "grey")

    (t
     :inverse-video t)
     
     )
     
  "Tab bar face.")

Here we have the general structure of an expression that evaluates multiple conditions. It looks like cond:

(cond
 ((FIRST TEST)
  FIRST RESULT)
 ((SECOND TEST)
  SECOND RESULT)
 (t                                     ; if none of the above
  FALLBACK RESULT))

With these in mind, we can read each test more easily. Focus on this:

(
 ((class color) (min-colors 88))
 :inherit variable-pitch
 :background "grey85"
 :foreground "black")

It checks whether the display terminal can support a minimum of 88 colours. If you are using Emacs with a graphical toolkit, this is most likely the case. If the condition is satisfied, this face will use grey85 for its background and black for its foreground. Whereas in more limited display terminals, it uses something simpler:

(
 ((class mono))
 :background "grey")

Same principle for the fallback condition, which merely inverts the colours with the assumption that those are some variant of black and white for the foreground/background:

(t
 :inverse-video t)

While you could define all your faces to adapt to every possible display terminal out there, I find that what one typically needs is to optimise for ((class color) (min-colors 89)).

With these in mind, we can start writing our first theme.

Skeleton of a custom theme

As noted in the previous section, Emacs offers custom-theme-set-faces for the express purpose of streamlining the process of controlling faces in bulk. As always, read the documentation of that function to learn more about the finer points.

Here we will be working with a minimal, yet perfectly usable base. Every theme must be placed in a file whose name follows the pattern of SYMBOL-theme.el. We declare the symbol of our theme with the following:

(deftheme prot-base
  "The basis for a custom theme.")

The above means that the file name must be prot-base-theme.el (we have some more code at the end of the file, but we take things one step at a time).

Now we want to configure a set of faces that are optimised for the display spec of ((class color) (min-colors 89)). Instead of writing this expression each time, we will dynamically bind it to a variable, using let.

(let ((class '((class color) (min-colors 89)))
      ...other variables)
    ...body)

Since we are defining local variables, it is a good idea to also write our colours here, so that we economise on typing, but also to avoid discrepencies. Each colour is defined as (name value).

(let ((class '((class color) (min-colors 89)))
      (main-bg "#ffffff") (main-fg "#000000")
      (red "#a00000") (green "#005000") (blue "#000077"))
    ...body)

Everything is in place to start defining face attributes. The body of our dynamically bound variables contains custom-theme-set-faces, followed by the name of the deftheme we declared and then each face’s symbol, display spec and attributes:

(deftheme prot-base
  "The basis for a custom theme.")

(let ((class '((class color) (min-colors 89)))
      (main-bg "#ffffff") (main-fg "#000000")
      (red "#a00000") (green "#005000") (blue "#000077"))
  (custom-theme-set-faces
   'prot-base
   `(default ((,class :background ,main-bg :foreground ,main-fg)))
   `(cursor ((,class :background ,red)))
   `(font-lock-builtin-face ((,class :foreground ,blue)))
   `(font-lock-string-face ((,class :foreground ,green)))))

(provide-theme 'prot-base)

(provide 'prot-base-theme)

This is a valid theme. To actually use it, you must write it to a file, which in this case is prot-base-theme.el. This file must be in a directory read by Emacs. Say you put it in ~/.emacs.d/themes/. To inform Emacs about it, evaluate this:

(add-to-list 'custom-theme-load-path "~/.emacs.d/themes/")

With the theme written at ~/.emacs.d/themes/prot-base-theme.el, you can now M-x load-theme RET prot-base. And there you have it!

Note though that you may also need to M-x disable-theme and specify the one currently in use to make sure you do not get mixed results (unless you want to overlay one theme on top of another, but I will let you run such experiments).

Remember to rely on list-faces-display to find all the symbols you wish to cover. Furthermore, you can always identify the properties of the character at point with M-x describe-char (or type it directly with C-u C-x =). If it uses a face, you will see it mentioned in the resulting *Help* buffer.

To understand the syntax for backquotes and commas, type M-: and then insert (info "(elisp) Backquote"). This will take you to the relevant node in the Emacs Lisp Reference Manual.

More tools for theme developers

These are excerpts from my dotemacs. They are meant to further assist you in the task of developing a custom theme. Check the doc string of each variable and adapt things to your liking.

Rainbow mode for colour previews

While experience may help estimate with decent accuracy a hexadecimal RGB colour, it is always better to have a live preview available. Once the following package is loaded, you can get it with M-x rainbow-mode.

(use-package rainbow-mode
  :ensure
  :diminish
  :commands rainbow-mode
  :config
  (setq rainbow-ansi-colors nil)
  (setq rainbow-x-colors nil))

Use a linter front-end to improve your code

You can either rely on the built-in flymake or the third party flycheck. Both work great with Elisp files. You activate them with flymake-mode or flycheck-mode respectively.

(use-package flymake
  :commands flymake-mode
  :config
  (setq flymake-fringe-indicator-position 'left-fringe)
  (setq flymake-suppress-zero-counters t)
  (setq flymake-start-on-flymake-mode t)
  (setq flymake-no-changes-timeout nil)
  (setq flymake-start-on-save-buffer t)
  (setq flymake-proc-compilation-prevents-syntax-check t)
  (setq flymake-wrap-around nil))

(use-package flycheck
  :ensure
  :commands flycheck-mode
  :config
  (setq flycheck-check-syntax-automatically
        '(save mode-enabled))
  :hook (flycheck-error-list-mode-hook . visual-line-mode))

If you go with Flycheck, you may also want a modeline indicator, unless you use a custom modeline that already defines one:

(use-package flycheck-indicator
  :ensure
  :after flycheck
  :config
  (setq flycheck-indicator-icon-error (string-to-char "!"))
  (setq flycheck-indicator-icon-info (string-to-char "·"))
  (setq flycheck-indicator-icon-warning (string-to-char "*"))
  (setq flycheck-indicator-status-icons
        '((not-checked "%")
          (no-checker "-")
          (running "&")
          (errored "!")
          (finished "=")
          (interrupted "#")
          (suspicious "?")))
  :hook (flycheck-mode-hook . flycheck-indicator-mode))

And here is how to ensure that you are following best practices for packaging Elisp libraries (you only need one of the two, depending on the front-end you choose):

(use-package flycheck-package
  :ensure
  :after flycheck
  :config
  (flycheck-package-setup))

(use-package package-lint-flymake
  :ensure
  :after flymake
  :config
  (package-lint-flymake-setup))

Remember that Emacs themes are Elisp programs

It should be clear by now that a theme can rely on advanced programming techniques to do its work. Here we used let. While you can always go with something simple, you retain the option to define more elaborate criteria that, say, come into effect once a certain variable is enabled.

My Modus themes, which were recently added to upstream Emacs, contain lots of Elisp logic, making them highly customisable. Study their source code if you want. It can help you learn more about defining and then evaluating customisation options.

Use the information in this document to write your own theme or to just gain insight into how the theme of your choice is designed.

Good luck!