11 Aug 2025

A How-to Guide to Font Selection

Motivation. There are two practical considerations to move beyond the default fonts. First, defaults are designed to be universally applicable, which inevitably makes them suboptimal for specific situations. Different types of texts—such as research papers, technical manuals, personal blogs, or software user interfaces—require different fonts to establish the right tone and clearly convey meaning without distracting their readers. Second, from a practical standpoint, most fonts are created to support only a limited subset of characters and styles. In practice, we have to deal with coverage issues and need to pair fonts to handle a wide range of characters, especially in non-English texts.

I start by clarifying some terminology and introduce the general principles for choosing fonts. Based on that, I discuss a few practical topics: previewing fonts, querying fonts, inspecting fonts, and using fonts. At the end, I put additional information in the appendices for interested readers.

Terminology

Terms used in typography, web development, and other fields may vary a lot. To avoid possible confusions, a brief list is provided below for reference. More terms and explanations can be found in the Glossary provided by Google Fonts, The Vocabulary of Type in Tracy (2003), and Appendix C: Glossary of Terms in Bringhurst (2002).

Glyph.

A glyph is a specific, visual representation of a character. Glyphs are the visual forms; characters are the abstract text elements.

In traditional typography, a glyph is literally a physical piece of metal or wood. In early digital typography, a glyph is a bitmap image—a pattern of tiny pixels that, when viewed together, resembles the shape of a character. In modern digital typography, a glyph is a vector outline—a flexible, resolution-independent vector graphic used to render a character visually.

A single character may have multiple glyphs (see alternates), and a glyph may represent multiple characters (see ligatures).

Font.

A font is a set of glyphs.

In traditional typography, a font is a physical plate with all its glyphs. In modern digital typography, a font is the digital palette itself or the digital information encoding it (a font file). Currently, most fonts are available in TrueType (.ttf, .ttc), OpenType (.otf, .otc), or Web Open Font Format (.woff, .woff2).

Font family

A font family is a collection of fonts that share the same design.

Each font in a family often has a specific size and weight. For example, the font DejaVu Sans-12:bold has a size of 12 points and a weight of bold. The font family, in this case, is DejaVu Sans.

In addition, a superfamily is a group of related font families. For example, the superfamily DejaVu contains font families DejaVu Serif, DejaVu Sans, and DejaVu Sans Mono.

How-to: Choose Fonts

The general principle for choosing fonts is to ensure that they effectively convey the intended meaning of the text. To achieve this, one need to check:

  1. The selected fonts are appropriate for the project. To avoid issues, choose fonts that were designed for the intended use.
  2. The selected fonts offer enough style variants to reflect text structures. For example, make sure there are suitable styles for emphasis, headlines, verbatim texts, and code sections.
  3. The selected fonts provide adequate script coverage (e.g., Greek and CJK).
  4. (Optional) The selected fonts support advanced typographic features. Look for features such as ligatures, alternative stylistic sets, and proper handling of punctuation for CJK characters.

For detailed considerations, please refer to Choosing type.

How-to: Preview Fonts

If a font family is accessible on the machine, then a convenient way to preview it is through the web browser. For example, the following HTML template previews the regular, italic, bold, and the small caps variant in Lora family.

<div class="font-section" style="font-family: lora;">
  <p style="font-size: xx-large; font-variant: small-caps">Lora:</p>
  <p>
    <b>Theorem (Arzelà–Ascoli).</b> Consider a set of real-valued continuous
    functions defined on a compact metric space. If this set is
    <i>equicontinuous</i> and <i>pointwise bounded</i>, then it is relatively compact in
    the topology induced by the <i>uniform</i> norm.
  </p>
</div>
preview-lora.png

For preview results of additional fonts, see also Appendix: A List of My Preferred Fonts. To list fonts available to the operating system, see also Appendix: Query Fonts via Fontconfig. Given a font file in .ttf or .otf format, one can use otfinfo or this online tool to view various metadata of the font; see also Appendix: Inspect OpenType Font Files.

How-to: Query Fonts

Fontconfig is a free and open-source software library designed to provide font discovery, configuration, and substitution functionality on Linux. It comes with a set of command-line utilities fc-*. Some of their usages are demonstrated below.

fc-list. List and filter fonts Fontconfig knows about.

fc-list :family=lora:style=italic file
/home/dou/.local/share/fonts/opentype/Lora/Lora-Italic.otf:
/home/dou/.local/share/fonts/opentype/Lora/Lora-SemiBoldItalic.otf:
/home/dou/.local/share/fonts/opentype/Lora/Lora-MediumItalic.otf:

fc-match. Find the font that matches a given pattern.

fc-match "lora-8:slant=italic:weight=medium" file
:file=/home/dou/.local/share/fonts/opentype/Lora/Lora-MediumItalic.otf

fc-pattern. Parse and show pattern according to Fontconfig's syntax.

fc-pattern "lora-8:slant=italic:weight=medium"
Pattern has 4 elts (size 16)
    family: "lora"(s)
    slant: 100(i)(s)
    weight: 100(f)(s)
    size: 8(f)(s)

See more explanations in fontconfig user documentation.

How-to: Inspect Fonts

OpenType (.otf) is an extension of TrueType (.ttf), and both are widely supported font formats on modern systems. Besides the glyph table, an OpenType font file also encodes various metadata and other font information to provide advanced typographic capabilities; see also OpenType Layout Overview. The online tool Font Drop can be used to inspect these data. Below, I demonstrate the usage of otfinfo.

The command-line utility otfinfo can report scripts and language systems supported in the font.

otfinfo -s sarasa-term-sc-nerd-regular.ttf
...
hani		CJK Ideographic
hani.JAN	CJK Ideographic/Japanese
hani.KOR	CJK Ideographic/Korean
hani.ZHH	CJK Ideographic/Chinese, Hong Kong SAR
hani.ZHS	CJK Ideographic/Chinese Simplified
hani.ZHT	CJK Ideographic/Chinese Traditional
...
latn		Latin
latn.JAN	Latin/Japanese
latn.KOR	Latin/Korean
latn.ZHH	Latin/Chinese, Hong Kong SAR
latn.ZHS	Latin/Chinese Simplified
latn.ZHT	Latin/Chinese Traditional

This shows that it supports two script systems: Latin and CJK. For CJK scripts, it supports five language systems: Simplified Chinese, Traditional Chinese, Chinese HK, Japanese, and Korean.

Supported OpenType features can also be printed via otfinfo.

otfinfo -f Lora-Regular.otf
aalt	Access All Alternates
calt	Contextual Alternates
case	Case-Sensitive Forms
ccmp	Glyph Composition/Decomposition
frac	Fractions
kern	Kerning
liga	Standard Ligatures
mark	Mark Positioning
mkmk	Mark to Mark Positioning
ordn	Ordinals
pnum	Proportional Figures
sups	Superscript
tnum	Tabular Figures

For font collections (.ttc or .otc), one can inspect encoded tables or extract individual fonts via Fonttools.

from fontTools.ttLib import TTCollection
from pathlib import Path

filepath = Path(...)
ttc = TTCollection(filepath)

# list opentype features
for i, font in enumerate(ttc.fonts):
    print(f"Font index {i}:")
    if "GSUB" in font:
        gsub = font["GSUB"]
        featureList = gsub.table.FeatureList
        if featureList:
            featureSet = set()
            for record in featureList.FeatureRecord:
                featureSet.add(record.FeatureTag)
            print(sorted(list(featureSet)))

# unpack the font collection
for i, font in enumerate(ttc.fonts):
    output_path = f"{filepath.stem}_{i}.ttf"
    try:
        font.save(str(output_path))  # font.save() expects a string path
        print(f"Extracted: {output_path}")
    except Exception as e:
        print(f"Error saving font {i}: {e}")

How-to: Use Fonts

This is the final application, and perhaps also the most frustrating part in practice.

There are actually two separate challenges: a design aspect and a technical aspect. From the design perspective, one needs to decide which piece of text should use which font. From a technical part, one needs to know how to set a specific piece of text so that it was displayed with a chosen font.

The first aspect depends on personal taste. You may copy others' styles, follow general rules of thumb, or consult typography books. Techniques discussed in the previouse sections may also be helpful.

The second aspect depends on applications being used. Different applications may implement their own text-rendering pipeline, and in general, there are no standard ways to configure them; see also Appendix: Text Rendering for a brief overview of the rendering process. Although most applications support straightforward font specification, advanced usages like dealing with missing glyphs may not be as intuitive; see also Appendix: Missing Characters and Fallbacks for more details.

References

Books
Bringhurst, R. (2002). The elements of typographic style. Hartley and Marks.
Tracy, W. (2003). Letters of credit: A view of type design. David R. Godine.
Online resources

Fontconfig user documentation. https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-user.html.

Fonttools. https://github.com/fonttools/fonttools

Google fonts knowledge. https://fonts.google.com/knowledge.

MDN web docs. Font. https://developer.mozilla.org/en-US/docs/Web/CSS/font.

OpenType® specification version 1.9.1. https://learn.microsoft.com/en-us/typography/opentype/spec/

Appendix: Text Rendering

According to Microsoft's OpenType Specifications, a string of characters codes are rendered by following a standard process summarized below.

  1. Convert a string of characters into a sequence of character codes.
  2. Convert the character codes into a sequence of glyph indices.
  3. Modify, substitute, and position the glyphs.
  4. Rasterizes the line of glyphs and renders the glyphs in device coordinates that correspond to the resolution of the output device.

Here, a character code (or code point) is simply an integer, which uniquely identifies a specific character within the Unicode standard. These integers are often experssed in hexadecimal (base-16) format, using the prefix "U+". For example, the letter "A" is assigned the integer 65, which is represented as "U+0041".

Appendix: A List of My Favorite Fonts

Below is a collection of my favorite fonts and their attributes; see previews here. The column Supported Scripts considers only: Latin, Greek, Math, and CJK. The column OpenType Features considers only: ligatures, fractions, small capitals, superscripts, and subscripts.

Font Family Properties and Tags Supported Scripts Style Variants OpenType Features Recommended Usage Additional Info
DejaVu Serif Serif Latin, Greek Regular, Italic, Bold, Bold Italic Ligatures Print, documents, body text Good Unicode coverage, Free & Open Source
DejaVu Sans Sans-serif Latin, Greek Regular, Oblique, Bold, Bold Oblique Ligatures UI, web design, documentation Readable at small sizes, Free & Open Source
DejaVu Sans Mono Monospace Latin, Greek Regular, Oblique, Bold, Bold Oblique N/A Coding, terminals Popular with programmers, Free & Open Source
Fira Sans Sans-serif Latin, Greek Regular, Italic, Bold, Bold Italic Fractions, Ligatures, Small Capitals, Subscripts, Superscripts UI, body text, print Modern, legible design, Free & Open Source
Fira Code Monospace Latin, Greek Regular, Bold Fractions, Subscripts, Superscripts Code editors, programming Specialized for programming, includes ligatures for code
Lora Serif Latin Regular, Italic, Bold, Bold Italic Fractions, Ligatures, Superscripts Editorial, web, blogs, print Contemporary with roots in calligraphy, Google Fonts
Noto Serif CJK SC Serif Latin, Greek, CJK Regular, Bold Ligatures Multilingual documents, CJK support Excellent for combining CJK & Latin, Google Fonts
Noto Sans CJK SC Sans-serif Latin, Greek, CJK Regular, Bold Ligatures UI, documents, multilingual text Excellent for CJK and Latin, Google Fonts
Noto Sans Mono CJK SC Monospace Latin, Greek, CJK Regular, Bold Ligatures Programming, multilingual coding Covers East Asian monospace needs, Google Fonts

Appendix: Missing Characters and Fallbacks

As of Unicode version 16.0, there are 292,531 assigned characters with code points, covering 168 modern and historical scripts, as well as multiple symbol sets.

— Wikipedia. List of Unicode Characters.

No single font can include the entire range of Unicode characters. Most fonts are designed to support only three or four script systems. As a result, we need to combine multiple fonts to display all characters correctly. This practice is commonly known as font fallback.

There are two common ways to define fallback fonts: character-level and range-level.

The character-level method uses a list of fonts. When a character’s glyph is required, the application checks each font in order and uses the first one that provides the glyph. Most applications support this approach. The example below shows the result of character-level fallback using the font list [Lora, Noto Serif]. Lora provides a glyph for "π", but "λ" and "σ" are missing, so they fall back to Noto Serif. This results in inconsistent typography.

lora-and-noto-character-level-fallback.png

The range-level method works more like assembling a custom font. It lets you assign specific fonts to particular subsets of Unicode characters. Many modern applications also support this approach. The example below shows the result of range-level fallback, where greek letters are displayed with Noto Serif and other characters are displayed with Lora.

lora-and-noto-range-level-fallback.png

Below are some details about font fallbacks in several applications.

HTML

For HTML, the character-level fallback can be achieved by simply specifying a list of fonts.

<div class="font-section" style="font-family: Lora, Noto Serif;">
<p>
Lora and Noto (Character-Level Fallback)
</p>

<p>
<b>Theorem (π-λ theorem).</b> A λ-system contains a π-system
<i>if and only if</i> it also contains the σ-algebra generated by that π-system.</p>
</div>

To achieve the range-level fallback, use the at-rule @font-face to declare a new font "Lora Noto" and load it.

@font-face {
  font-family: "Lora Noto";
  src: local("Lora Regular");
  font-style: regular;
}

@font-face {
  font-family: "Lora Noto";
  src: local("Lora Italic");
  font-style: italic;
}

/* Override for Greek characters: Noto Serif (Greek) */
@font-face {
  font-family: "Lora Noto";
  src: local("Noto Serif Regular");
  font-style: regular;
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}

@font-face {
  font-family: "Lora Noto";
  src: local("Noto Serif Italic");
  font-style: italic;
  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
<div class="font-section" style="font-family: Lora Noto;">
<p>
Lora Noto (Range-Level Fallback)
</p>

<p>
<b>Theorem (π-λ theorem).</b> A λ-system contains a π-system
<i>if and only if</i> it also contains the σ-algebra generated by that π-system.</p>
</div>

Emacs

For Emacs, the character-level fallback can be achieved[1] by setting the variable face-font-family-alternatives.

(custom-set-variables
 '(face-font-family-alternatives
   '(("Monospace" "Noto Sans Mono" "Noto Sans Mono CJK SC")
     ("Sans Serif" "Noto Sans" "Noto Sans CJK SC")
     ("Serif" "Noto Serif" "Noto Serif CJK SC")
     ("Monospace Serif" "Courier Prime")
     ("Fira Sans" "Noto Sans" "Noto Sans CJK SC")
     ("FiraCode Nerd Font Ret" "Noto Sans Mono" "LXGW WenKai Mono")
     ("Lora" "Noto Serif" "Noto Serif CJK SC")
     ("Noto Serif" "Noto Serif CJK SC")))
 )
(set-face-attribute 'org-verse nil :family "Lora" :inherit nil)

To achieve the range-level fallback, use custom fontsets. For example, we can define a new fontset "fontset-orgverse" and use it to display text within org verse blocks[2]. Note that the fontset attribute might be conflict with the font attribute or the family attribute. Run describe-face RET org-verse to inspect all attributes of the face "org-verse", or execute (face-spec-reset-face 'org-verse) to reset the face.

(setq dms/fontset-org-verse
      (create-fontset-from-fontset-spec
       (font-xlfd-name (font-spec :registry "fontset-orgverse")))
)

(set-fontset-font dms/fontset-org-verse 'latin
                  "Lora")
(set-fontset-font dms/fontset-org-verse 'greek
                  "Noto Serif")
(set-fontset-font dms/fontset-org-verse 'han
                  "Noto Serif CJK SC")
(set-fontset-font dms/fontset-org-verse 'cjk-misc
                  "Noto Serif CJK SC")

(set-face-attribute 'org-verse nil :font dms/fontset-org-verse :fontset dms/fontset-org-verse :inherit nil)

Fontconfig

To the best of my knowledge, fontconfig does not support range level fallback. The character-level fallback is by default enabled.

Fontconfig performs matching by measuring the distance from a provided pattern to all of the available fonts in the system. The closest matching font is selected. This ensures that a font will always be returned, but doesn't ensure that it is anything like the requested pattern.

To see the list of matched fonts of a given pattern, use fc-match -s. To manually configure how a pattern is matched, use font configuration files. A typical path for such a configuration file is ~/.config/fontconfig/fonts.conf; see here for other possible paths of configuration files.

The example configuration file below demonstrates: 1) define an alias; 2) add fallback fonts of a specific font; 3) replace a font if condition satisfied.

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <alias>
         <family>serif</family>
         <prefer>
             <family>Noto Serif</family>
             <family>Noto Serif CJK SC</family>
         </prefer>
    </alias>

    <!-- fallback font of lora -->
    <match target="pattern">
        <test name="family" compare="eq">
            <string>lora</string>
        </test>
        <edit name="family" mode="append">
            <string>Noto Serif</string>
            <string>Noto Serif CJK SC</string>
        </edit>
    </match>

    <!-- respect lang=ja (Japanese) -->
    <match>
        <test name="lang" compare="eq">
            <string>ja</string>
        </test>
        <test name="family" compare="eq">
            <string>Noto Serif CJK SC</string>
        </test>
        <edit name="family" mode="prepend" binding="strong">
            <string>Noto Serif CJK JP</string>
        </edit>
    </match>
</fontconfig>

Footnotes:

[1]

This method works in my settings, but is not documented in the docstring of face-font-family-alternatives. Moreover, it will be ignored if fontset-default has suggested font for the missing glyph.

[2]

Of course, you need to sure org-fontify-quote-and-verse-blocks is not nil. Otherwise, Org will not fontify quote or verse blocks at all.

Tags: tool
Created by Org Static Blog