← Back to DevBytes

Emacs Themes and Customization: Complete Guide

Understanding Emacs Themes and Faces

Emacs is not just a text editor—it's a fully customizable environment where every pixel, color, and font can be tailored to your preferences. At the heart of this visual customization system lie two interconnected concepts: themes and faces.

A face in Emacs is a named collection of visual attributes—foreground color, background color, font family, weight, slant, underline, and more—that controls how a specific piece of text is displayed. Every element in Emacs, from the cursor to the mode line to syntax-highlighted keywords, is rendered through a face. A theme is essentially a bundled collection of face definitions that can be loaded, unloaded, and switched between seamlessly, transforming the entire appearance of your editor in a single command.

This dual-layer architecture gives you extraordinary power: you can either adopt a complete visual package via a theme, or surgically adjust individual faces to create a look that is entirely your own.

Why Customizing Emacs Appearance Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Investing time in theming and customization yields tangible benefits that compound over the thousands of hours you'll spend in your editor:

Exploring Built-in Themes

Modern Emacs (version 24 and above) ships with a curated selection of themes that you can try immediately without any external packages. To browse the available built-in themes:

;; List all available themes on your system
M-x customize-themes RET

This opens the Custom Themes browser, showing checkboxes for each installed theme. You can toggle them on and off interactively to preview combinations. Alternatively, load a theme programmatically:

;; Load a built-in theme instantly
(load-theme 'tango-dark t)   ; t means "no confirmation prompt"

;; Some popular built-in themes you can try:
;; - tango-dark, tango-light
;; - wombat
;; - adwaita
;; - deeper-blue
;; - dichromacy (for color-blind users)
;; - leuven
;; - misterioso
;; - tsdh-dark, tsdh-light
;; - whiteboard
;; - manoj-dark

To see which theme is currently active and get a description of each face it defines:

;; Describe the currently active theme
M-x describe-theme RET

Enabling Multiple Themes

Emacs allows you to layer themes. When you load multiple themes, faces defined in later-loaded themes override earlier ones. This is useful for creating hybrid looks:

;; Load a base theme, then layer a secondary theme on top
(load-theme 'tango-dark t)
(load-theme 'dichromacy t)  ; applies color-blind friendly adjustments on top

To unload a specific theme without affecting others:

;; Remove only one theme from the layered stack
(disable-theme 'dichromacy)

Installing Third-Party Themes

The true breadth of Emacs theming emerges when you tap into the community ecosystem. Hundreds of meticulously crafted themes are available through package archives like MELPA.

Step 1: Configure Package Archives

Ensure your Emacs can reach MELPA (the most comprehensive package repository):

;; Add to your init.el or .emacs
(require 'package)

;; Define archive sources
(setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                         ("melpa" . "https://melpa.org/packages/")))

;; Initialize package system
(package-initialize)
(package-refresh-contents)

Step 2: Discover and Install Themes

You can browse themes directly from Emacs:

;; List packages matching "theme" in their name
M-x package-list-packages RET
;; Then press / and type: theme

Or install a known theme programmatically:

;; Install a theme package (only once)
(unless (package-installed-p 'doom-themes)
  (package-install 'doom-themes))

;; Install several popular theme packages
(unless (package-installed-p 'modus-themes)
  (package-install 'modus-themes))

(unless (package-installed-p 'dracula-theme)
  (package-install 'dracula-theme))

(unless (package-installed-p 'gruvbox-theme)
  (package-install 'gruvbox-theme))

(unless (package-installed-p 'spacemacs-theme)
  (package-install 'spacemacs-theme))

(unless (package-installed-p 'nord-theme)
  (package-install 'nord-theme))

Step 3: Load the Installed Theme

;; Load a third-party theme
(load-theme 'doom-one t)

;; Or conditionally load based on time of day
(let ((hour (string-to-number (format-time-string "%H"))))
  (if (and (> hour 6) (< hour 18))
      (load-theme 'modus-operandi t)   ; light theme for daytime
    (load-theme 'modus-vivendi t)))    ; dark theme for nighttime

Notable Third-Party Theme Packages

Understanding Faces in Depth

A face is a symbol that holds a property list of visual attributes. You can inspect any face interactively:

;; Open the face description for a specific face
M-x describe-face RET font-lock-keyword-face RET

The output shows something like:

Face: font-lock-keyword-face
Defined in: font-lock.el
Attributes:
  :foreground "#a020f0"
  :weight bold
  :slant unspecified
  :underline unspecified
  :height unspecified

Every attribute that is unspecified inherits from a parent face or the default face. This cascading inheritance is powerful: by tweaking just the default face, you can affect the entire editor's baseline appearance.

Key Face Attributes

Customizing Individual Faces

You don't need to create an entire theme to make targeted improvements. Emacs provides multiple pathways for face customization, from interactive UI to programmatic Lisp.

Method 1: The Customize Interface (Interactive)

;; Open the face customizer for a specific face
M-x customize-face RET default RET

;; Browse all faces grouped by category
M-x list-faces-display RET

In the customization buffer, you can change attributes, see a live preview, and save changes to your init file. When you save, Emacs writes the settings using the custom-set-faces function:

;; What Emacs writes to your init file when you save via customize
(custom-set-faces
 ;; custom-set-faces was added by Custom.
 ;; If you edit it by hand, the magic will go away.
 ;; You must restart Emacs or eval this block.
 '(default ((t (:family "Fira Code" :height 140 :weight normal))))
 '(font-lock-comment-face ((t (:slant italic :foreground "#8b8b8b")))))

Method 2: Programmatic Face Customization

For full control, use set-face-attribute directly in your init file:

;; Set the default face — this affects everything that inherits from it
(set-face-attribute 'default nil
  :family "JetBrains Mono"
  :height 130
  :weight 'normal
  :foreground "#e0e0e0"
  :background "#1e1e1e")

;; Customize syntax highlighting faces
(set-face-attribute 'font-lock-keyword-face nil
  :foreground "#569cd6"
  :weight 'bold)

(set-face-attribute 'font-lock-string-face nil
  :foreground "#ce9178"
  :slant 'italic)

(set-face-attribute 'font-lock-comment-face nil
  :foreground "#6a9955"
  :slant 'italic)

(set-face-attribute 'font-lock-function-name-face nil
  :foreground "#dcdcaa")

(set-face-attribute 'font-lock-variable-name-face nil
  :foreground "#9cdcfe")

(set-face-attribute 'font-lock-type-face nil
  :foreground "#4ec9b0")

(set-face-attribute 'font-lock-constant-face nil
  :foreground "#4fc1ff")

(set-face-attribute 'font-lock-warning-face nil
  :foreground "#f44747"
  :weight 'bold)

Method 3: Using the Custom Theme Format

For sharing your configurations or applying them as a cohesive set, use custom-theme-set-faces:

;; Define a set of face overrides in theme format
(custom-theme-set-faces 'user-theme
 '(default ((t (:background "#282c34" :foreground "#abb2bf"))))
 '(cursor ((t (:background "#528bff"))))
 '(region ((t (:background "#3e4451"))))
 '(mode-line ((t (:background "#383b42" :foreground "#979797" :box nil))))
 '(mode-line-inactive ((t (:background "#282c34" :foreground "#5c6370" :box nil))))
 '(line-number ((t (:foreground "#4b5263"))))
 '(line-number-current-line ((t (:foreground "#abb2bf" :weight bold))))
 '(header-line ((t (:background "#282c34" :foreground "#98be65"))))
 '(fringe ((t (:background "#282c34"))))
 '(hl-line ((t (:background "#2c323b"))))
 '(link ((t (:foreground "#61afef" :underline t))))
 '(link-visited ((t (:foreground "#c678dd" :underline t))))
 '(highlight ((t (:background "#3a3f4b")))))

Customizing the Mode Line

The mode line is one of the most visible elements of Emacs, yet it's often left in its default state. A well-designed mode line provides information at a glance without being distracting.

Understanding Mode Line Faces

The mode line uses several specialized faces:

Basic Mode Line Styling

;; Give the active mode line a distinctive look
(set-face-attribute 'mode-line nil
  :background "#383b42"
  :foreground "#979797"
  :box '(:line-width 1 :color "#4a4f59")
  :height 0.9)

;; Make inactive mode lines subtle and unobtrusive
(set-face-attribute 'mode-line-inactive nil
  :background "#282c34"
  :foreground "#5c6370"
  :box '(:line-width 1 :color "#282c34")
  :height 0.9)

;; Highlight buffer name in the mode line
(set-face-attribute 'mode-line-buffer-id nil
  :foreground "#61afef"
  :weight 'bold)

Third-Party Mode Line Packages

Several packages replace the mode line entirely with richer, more configurable alternatives:

;; doom-modeline — modern, information-rich mode line
(unless (package-installed-p 'doom-modeline)
  (package-install 'doom-modeline))
(doom-modeline-mode 1)

;; moody — minimal, elegant mode line with vertical tabs feel
(unless (package-installed-p 'moody)
  (package-install 'moody))
(moody-mode 1)

;; mini-modeline — shrink the mode line to a single tiny line
(unless (package-installed-p 'mini-modeline)
  (package-install 'mini-modeline))
(mini-modeline-mode 1)

Font Configuration for Coding

Typography is a cornerstone of a comfortable editing environment. Emacs handles fonts through the default face and can use different fonts for different purposes.

Setting the Main Coding Font

;; Set a monospaced coding font with ligature support
(set-face-attribute 'default nil
  :family "Fira Code"
  :weight 'light
  :height 135)

;; Alternative: Use a different font for specific modes via hooks
(defun my-set-prose-font ()
  "Switch to a variable-pitch font for prose modes."
  (set-face-attribute 'default nil :family "Iosevka Etoile" :height 150))

(add-hook 'org-mode-hook 'my-set-prose-font)
(add-hook 'markdown-mode-hook 'my-set-prose-font)

Working with Variable-Pitch Fonts in Org Mode

Org mode supports mixing monospaced and variable-pitch fonts beautifully. Set the base face to a variable-pitch font for prose, while keeping code blocks monospaced:

;; In org-mode, use variable-pitch for body text
(add-hook 'org-mode-hook
          (lambda ()
            (set-face-attribute 'default nil :family "ET Book" :height 160)
            ;; Ensure code blocks stay monospaced
            (set-face-attribute 'org-block nil :family "Fira Code" :height 130)
            (set-face-attribute 'org-code nil :family "Fira Code" :height 130)
            (set-face-attribute 'org-table nil :family "Fira Code" :height 130)))

Creating Your Own Theme from Scratch

Creating a custom theme is surprisingly straightforward. A theme file is simply a Lisp file that uses deftheme and custom-theme-set-faces (and optionally custom-theme-set-variables).

Anatomy of a Theme File

;;; my-awesome-theme.el — A complete custom Emacs theme

(deftheme my-awesome-theme
  "A warm, low-contrast dark theme optimized for long coding sessions.")

;; Define a color palette as variables for consistency
(let ((bg        "#1a1b26")   ; dark navy background
      (bg-alt    "#24283b")   ; slightly lighter background
      (fg        "#c0caf5")   ; soft white foreground
      (fg-dim    "#565f89")   ; dimmed text
      (red       "#f7768e")   ; errors, warnings
      (orange    "#ff9e64")   ; numbers, constants
      (yellow    "#e0af68")   ; strings
      (green     "#9ece6a")   ; success, functions
      (teal      "#1abc9c")   ; types
      (blue      "#7aa2f7")   ; keywords
      (purple    "#ad8ee6")   ; special symbols
      (magenta   "#bb9af7")   ; preprocessor
      (cyan      "#7dcfff")   ; regexp groups
      (comment   "#565f89"))  ; comments

  ;; Apply faces using the palette
  (custom-theme-set-faces 'my-awesome-theme

    ;; Core editor faces
    `(default ((t (:background ,bg :foreground ,fg))))
    `(cursor ((t (:background ,blue :foreground ,bg))))
    `(region ((t (:background ,bg-alt :foreground ,fg))))
    `(fringe ((t (:background ,bg))))
    `(hl-line ((t (:background ,bg-alt :extend t))))
    `(line-number ((t (:foreground ,fg-dim :background ,bg))))
    `(line-number-current-line ((t (:foreground ,fg :background ,bg-alt :weight bold))))

    ;; Syntax highlighting
    `(font-lock-keyword-face ((t (:foreground ,blue :weight bold))))
    `(font-lock-string-face ((t (:foreground ,yellow))))
    `(font-lock-comment-face ((t (:foreground ,comment :slant italic))))
    `(font-lock-function-name-face ((t (:foreground ,green))))
    `(font-lock-variable-name-face ((t (:foreground ,fg))))
    `(font-lock-type-face ((t (:foreground ,teal))))
    `(font-lock-constant-face ((t (:foreground ,orange))))
    `(font-lock-warning-face ((t (:foreground ,red :weight bold))))
    `(font-lock-builtin-face ((t (:foreground ,blue))))
    `(font-lock-preprocessor-face ((t (:foreground ,magenta))))
    `(font-lock-regexp-grouping-backslash ((t (:foreground ,cyan :weight bold))))
    `(font-lock-regexp-grouping-construct ((t (:foreground ,cyan))))

    ;; Search and match
    `(match ((t (:background ,blue :foreground ,bg :weight bold))))
    `(isearch ((t (:background ,orange :foreground ,bg :weight bold))))
    `(isearch-fail ((t (:background ,red :foreground ,bg))))
    `(lazy-highlight ((t (:background ,bg-alt :foreground ,orange))))

    ;; Mode line
    `(mode-line ((t (:background ,bg-alt :foreground ,fg :box (:line-width 1 :color ,bg-alt) :height 0.9))))
    `(mode-line-inactive ((t (:background ,bg :foreground ,fg-dim :box (:line-width 1 :color ,bg) :height 0.9))))
    `(mode-line-buffer-id ((t (:foreground ,blue :weight bold))))
    `(mode-line-emphasis ((t (:foreground ,yellow))))

    ;; UI elements
    `(header-line ((t (:background ,bg-alt :foreground ,fg))))
    `(vertical-border ((t (:foreground ,bg-alt))))
    `(minibuffer-prompt ((t (:foreground ,blue :weight bold))))
    `(link ((t (:foreground ,cyan :underline t))))
    `(link-visited ((t (:foreground ,purple :underline t))))
    `(button ((t (:foreground ,blue :box (:line-width 1 :color ,blue)))))

    ;; Org mode specific faces
    `(org-level-1 ((t (:foreground ,blue :weight bold :height 1.2))))
    `(org-level-2 ((t (:foreground ,green :weight bold :height 1.1))))
    `(org-level-3 ((t (:foreground ,teal :weight bold))))
    `(org-level-4 ((t (:foreground ,purple))))
    `(org-level-5 ((t (:foreground ,orange))))
    `(org-code ((t (:foreground ,yellow :background ,bg-alt))))
    `(org-block ((t (:background ,bg-alt))))
    `(org-block-begin-line ((t (:foreground ,comment :background ,bg-alt))))
    `(org-block-end-line ((t (:foreground ,comment :background ,bg-alt))))
    `(org-table ((t (:foreground ,fg))))
    `(org-date ((t (:foreground ,cyan))))
    `(org-todo ((t (:foreground ,red :weight bold))))
    `(org-done ((t (:foreground ,green :weight bold))))
    `(org-agenda-date ((t (:foreground ,blue :weight bold))))

    ;; Diff faces
    `(diff-added ((t (:background "#1b3d2c" :foreground ,green))))
    `(diff-removed ((t (:background "#3d1b2c" :foreground ,red))))
    `(diff-context ((t (:foreground ,fg-dim))))
    `(diff-file-header ((t (:background ,bg-alt :foreground ,fg :weight bold))))
    `(diff-hunk-header ((t (:background ,bg-alt :foreground ,purple))))

    ;; Dired faces
    `(dired-directory ((t (:foreground ,blue :weight bold))))
    `(dired-symlink ((t (:foreground ,cyan))))
    `(dired-marked ((t (:foreground ,orange :weight bold))))

    ;; Magit faces (if you use Magit)
    `(magit-branch ((t (:foreground ,blue :weight bold))))
    `(magit-hash ((t (:foreground ,fg-dim))))
    `(magit-section-heading ((t (:foreground ,green :weight bold))))
    `(magit-diff-added ((t (:background "#1b3d2c" :foreground ,green))))
    `(magit-diff-removed ((t (:background "#3d1b2c" :foreground ,red))))))

;;; Provide the theme so it can be loaded with load-theme
(provide-theme 'my-awesome-theme)
(provide 'my-awesome-theme)
;;; my-awesome-theme.el ends here

Installing and Loading Your Custom Theme

Place your theme file in ~/.emacs.d/themes/ (create the directory if needed), then add it to Emacs's search path:

;; Add your personal themes directory to the load path
(add-to-list 'custom-theme-load-path
             (expand-file-name "themes" user-emacs-directory))

;; Now you can load your theme like any other
(load-theme 'my-awesome-theme t)

;; Or make it the default and prevent the custom-theme-load-path
;; warning on startup
(setq custom-theme-directory
      (expand-file-name "themes" user-emacs-directory))

Dynamic Theme Switching

A powerful customization pattern is to switch themes based on external conditions like time of day, system appearance (dark/light mode), or even project type.

Time-Based Theme Switching

;; Automatically switch theme based on time of day
(defun my-load-time-appropriate-theme ()
  "Load a light or dark theme based on current hour."
  (let ((hour (string-to-number (format-time-string "%H" t))))
    (if (and (>= hour 6) (< hour 18))
        (progn
          (disable-theme 'modus-vivendi)
          (load-theme 'modus-operandi t t))
      (progn
        (disable-theme 'modus-operandi)
        (load-theme 'modus-vivendi t t)))))

;; Run on startup
(my-load-time-appropriate-theme)

;; Optionally, run periodically via a timer for seamless transitions
(run-at-time "07:00" (* 12 3600) 'my-load-time-appropriate-theme)
(run-at-time "18:30" (* 12 3600) 'my-load-time-appropriate-theme)

System Dark Mode Detection (macOS)

;; Detect macOS dark mode and switch Emacs theme accordingly
(defun my-sync-with-macos-dark-mode ()
  "Switch Emacs theme to match macOS appearance."
  (let ((dark-mode-p
         (string-equal
          (string-trim
           (shell-command-to-string
            "defaults read -g AppleInterfaceStyle 2>/dev/null"))
          "Dark")))
    (if dark-mode-p
        (progn
          (disable-theme 'modus-operandi)
          (load-theme 'modus-vivendi t t))
      (progn
        (disable-theme 'modus-vivendi)
        (load-theme 'modus-operandi t t)))))

;; Check on focus-in (when switching back to Emacs)
(add-hook 'focus-in-hook 'my-sync-with-macos-dark-mode)

Customizing the Cursor and Visual Indicators

Beyond text faces, Emacs offers fine-grained control over cursors, indicators, and UI chrome that affect your editing experience.

;; Cursor customization
(setq cursor-type 'bar)           ; vertical bar cursor
;; Alternatives: 'hbar (horizontal), 'box (block), '(bar . 2) (bar of width 2)
;; Or a hollow box: '(hollow . 2)

(setq cursor-in-non-selected-windows 'hollow)  ; hollow cursor in inactive windows
(setq x-stretch-cursor t)                       ; widen cursor to cover character width

;; Blink settings
(blink-cursor-mode 1)            ; enable blinking
(setq blink-cursor-interval 0.5) ; blink every 0.5 seconds
(setq blink-cursor-delay 0.2)    ; initial delay before blinking

;; Make the cursor color match your theme
(set-face-attribute 'cursor nil :background "#ff6c6c")

;; Highlight the current line
(global-hl-line-mode 1)
(set-face-attribute 'hl-line nil
  :background "#2c323b"
  :extend t)  ; extend background to the window edge

;; Show matching parentheses in a distinct color
(show-paren-mode 1)
(set-face-attribute 'show-paren-match nil
  :background "#3a3f4b"
  :weight 'bold)
(set-face-attribute 'show-paren-mismatch nil
  :background "#ff0000"
  :foreground "#ffffff"
  :weight 'bold)

;; Customize the region selection appearance
(set-face-attribute 'region nil
  :background "#3e4451"
  :foreground nil)  ; nil = don't change foreground, keep text readable

Working with the Custom System

Emacs's Custom system (custom.el) provides a structured way to manage settings. Understanding its interaction with themes is crucial for maintaining a clean configuration.

How Custom and Themes Interact

Settings saved via M-x customize are stored using custom-set-variables and custom-set-faces. These are separate from themes and persist across theme switches. The priority order is:

  1. Custom-set-faces (highest priority — saved by Custom interface)
  2. Loaded themes (in reverse order of loading — last loaded wins)
  3. Face default values (lowest priority)

This means if you customize a face via the Custom interface and save it, that setting will override any theme you load later. This can be confusing if you switch themes and wonder why certain faces aren't changing.

Resolving Conflicts

;; Check what's overriding a face
M-x customize-face RET default RET
;; Look at the "Theme" and "Custom" lines in the buffer

;; Reset a face to its theme-defined value
(customize-set-variable 'custom-theme-allow-override t)

;; Remove custom face settings entirely (use with caution)
(custom-set-faces)  ; empty call clears all custom faces

;; Or surgically reset a single face's custom override
(set-face-attribute 'font-lock-comment-face nil
  :foreground 'unspecified)  ; revert to theme default

Organizing Your Custom Settings

Many Emacs users prefer to keep Custom settings in a separate file to avoid cluttering their main configuration:

;; In your init.el, direct Custom to a separate file
(setq custom-file (expand-file-name "custom-settings.el" user-emacs-directory))

;; Load the custom settings file if it exists
(if (file-exists-p custom-file)
    (load custom-file)
  ;; Create it if it doesn't exist
  (write-region ";; Custom settings managed by Emacs\n" nil custom-file))

Performance Considerations

While theming is primarily visual, some choices can affect Emacs's rendering performance, especially on older hardware or in terminal frames.

;; Check if your Emacs supports true color in terminal
(if (eq (display-color-cells) 16777216)
    (message "True color terminal detected — full theme support available")
  (message "Limited color terminal — some theme colors may be approximated"))
;; Defer theme loading to after startup for faster init
(add-hook 'after-init-hook
          (lambda ()
            (load-theme 'modus-vivendi t)
            (run-with-timer 0.1 nil
              (lambda ()
                (set-face-attribute 'default nil :family "Fira Code" :height 135)))))

Best Practices for Theme Customization

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles