How to Define Emacs Custom Faces for text highlighting

Emacs already provided a set of faces, you can find them in font-lock.el. Most faces are designed for programming language syntax highlighting like keywords, string, constants, etc. If you need more fine-grained control to highlighting, you need to define custom faces.

Define Custom Faces

The following code defines three custom faces.

 
(defvar highlight 'highlight "Face name to use for highlight.")
(defface highlight
  '((((class color) (min-colors 88) (background light))    :foreground "#005071" :background "#E9FFD4")
    (t :inverse-video t))
  "Basic face for highlighting."
  :group 'basic-faces)
 
(defvar strong-bold 'strong-bold "Face name to use for strong.")
(defface strong-bold
  '((((class color) (min-colors 88) (background light))    :foreground "black" :weight bold )
    (((class color) (min-colors 88) (background dark))     :foreground "white" :weight bold )
    (t :inverse-video t))
  "Basic face for strong."
  :group 'basic-faces)
 
;(face-spec-set 'strong-bold
;  '((((class color) (min-colors 88) (background light))    :foreground "black" :weight bold :underline t)
;    (((class color) (min-colors 88) (background dark))     :foreground "white" :weight bold :underline t)
;    (t :inverse-video t))
;  'face-defface-spec
;)
 
(defvar sub-title 'sub-title "Face name to use for strong sub title.")
(defface sub-title
  '((((class color) (min-colors 88) (background light))    :foreground "black" :weight bold :underline t)
    (((class color) (min-colors 88) (background dark))     :foreground "white" :weight bold :underline t)
    (t :inverse-video t))
  "Basic face for sub title."
  :group 'basic-faces)
 

The ((class color) (min-colors 88) (background light)) is the predicate that matches the terminal where your Emacs is running. The t means match all terminal. Start from top, if any predicate is matched, the rest will be ignored. For custom faces, you can ignore many rarely used terminal and focus on the terminal you are using now, this will simplify your custom face definition.

One thing to notice is the defface macro can only set the custom face on the first time invocation, if you change the custom face definition and reevaluate the code in ELISP REPL, it won't take effect. To make changes to the custom face definition, use face-spec-set. It's great help when you are experimenting with difference faces, you can adjust the custom face definition until it's satisfying to you.

If reevaluate with face-spec-set doesn't work, you should check the value of variable defined by defvar, for example you can execute describe-variable and input sub-title, the value should be the same as the name defined by macro defface. To overwrite the defvar definition, you should use setq because defvar can only set the initial value, the subsequent defvar invocation won't change the value.

Use Custom Faces in your Major Mode

Let's drive a major mode from fundamental-mode and apply our custom faces to text that matches a regular expression.

 
(defun test-font-lock-extend-region ()
  "Extend the search region to include an entire block of text."
  ;; Avoid compiler warnings about these global variables from font-lock.el.
  ;; See the documentation for variable `font-lock-extend-region-functions'.
  (eval-when-compile (defvar font-lock-beg) (defvar font-lock-end))
  (save-excursion
    (goto-char font-lock-beg)
    (let ((found (or (re-search-backward "\n\n" nil t) (point-min))))
      (goto-char font-lock-end)
      (when (re-search-forward "\n\n" nil t)
        (beginning-of-line)
        (setq font-lock-end (point)))
      (setq font-lock-beg found))))
 
(define-derived-mode mytext-mode fundamental-mode "mytext"
  (setq mytext-highlights '(
                            ("code:hide\\(.\\|\n\\)*?code:end" . (0 font-lock-comment-face t))
                            ("<strong>.*?</strong>" . (0 strong-bold t))
                            ("^\\[.*?\\]\\]?" . (0 sub-title t))
                            ("<tt>.*?</tt>" . (0 highlight t))
                           )
  )
  (setq  font-lock-defaults '(mytext-highlights))
  (message "deriving mode mytext")
 
  (set (make-local-variable 'font-lock-multiline) t)
  (add-hook 'font-lock-extend-region-functions
            'test-font-lock-extend-region)
)
 

Now you can execute M-x mytext-mode, your text will be highlighted as below.