Add A Custom Theme to mdBook

What is mdBook?

mdBook is the open-source project that this website was built with. It's hosted on Github and it's project maintainers are the maintainers of Rust, my favorite programming language when I'm forced to write code. The project compiles to a single binary that one can use to compile markdown files and related assets to html. The location of the files is specified by convention and the project supports plugins that can modify the output using preprocessing. This website makes use of a few of these preprocessing tools at the moment. They are also compiled to a single binary, all of their filenames must start with the prefix mdbook-. mdBook is made aware of your intention to use these preprocessors by a simple specification in your website's book.toml. This website is currently using the following preprocessors:

This website also uses the following external tools. These are binaries that you will need to run in addition to the mdbook build command:

Why do you need a custom theme?

I need a custom theme because I don't particularly enjoy writing code. I enjoy writing stories and I'd like these stories to not hurt your or my eyes when reading them. I noticed that the built in theme's colors strained my eyes and made words blur together even though I'm wearing glasses. Books, for the most part, don't affect me like this unless the light is too low in my extremely dark apartment. It took a little bit of experimenting and a helpful blog post on non-violent fonts to find something that works for me. I have an elementary undersatnding of French so when I read that serif means "declorative flourish" I was astonished that I've gone all these years ignoring "without decorative flourish" while continuing to build apps and websites. I landed on Garamond which actually has a cool backstory, I had no idea fonts could be hundreds of years old. It's available for free at freefonts.com. As per mdBook convention the font will need to be converted to woff2 because the only available file type I could find was ttf. There are libraries available to do this on Github but because I am me, I would have ended up reading their source code, along with the source code of any important dependencies, so I just used this free online tool which works swimingly. Alternatively, if you like the font on this website you can just download it.

Here are the other fonts that came in the package that I have already converted from ttf to woff2. I can't answer with any specificity what they are to be used for but I didn't specify their use anywhere, so I'm assuming they're not being used anywhere. Browsers are massive projects so I could be wrong, but I'm definitely not looking it up tonight.

If you try my custom theme to read the stories contained in this website, and find that it strains your eyes, please send me an email! I want you to read these and I do not want you to hurt yourself doing so!! Check out my contact page for ways to contact me.

I've also found a dearth, neigh, a void, of theme options that don't involve replacing the built in themes, or that seem to only replace the code block themes. The two I played with were:

  • Catpuccin's repo - Really pretty themes for many different applications, but requires replacing the built in themes.
  • mdbook-theme - Actually seems really cool and gives you all of themes from ace editor, but involves a more intensive setup, only allows "theming" by modifying the css variables in a preprocessor which affects all themes, and only supplies all the really flowery theming to the live Rust code editor built into mdBook.

Is it hard to do?

Not really. A general overview is that you need recursively copy the fonts directory from your compiled book directory into your src directory and then add your font the newly copied directoy (src/fonts). You then recursively copy the book/theme directory to your src directory. Following that, you modify a couple of the .css files. Finally you add the menu option of your new theme to src/theme/index.hbs.

For convenience, here are the unix commands to initially build the required files and copy them to the correct locations.

mdbook build
cp -r book/fonts src
cp -r book/theme src

You can see the additions I made by diffing the commit that made the changes to the previous commit on my Github. Just scroll down to the files in question.

Will you explain the changes?

Sure, the src/theme/css/variables.css file contains variables that get imported into the other css files. They are defined as so:

.theme-name-css-class-selector {
    --variable-name: "value"
}

They are then used in the files they are imported into as so:

.css-class-selector {
    style-property: var(--variable-name)
}

This way, the values of those style properties dynamically change when a theme is selected from your website's theme menu options.

The src/theme/css/general.css file contains the base styles for the major div elements you see when you look at your website. For instance the html, body, code, and content tags. The content tags contain essentially everything that isn't navigation or interactivity. So basically, the pages in your book. We modify this file to use the variables we defined in src/theme/css/variables.css.

The src/theme/css/chrome.css contains extra styles of unspecified purpose, they are styles that the maintainers did not deem worthy of the "Base Style" mantle.

Using this knowledge we need only make a few changes to get the desired effect, a skinable custom theme that doesn't require replacing any of the existing themes.

First we add the font-family property to src/theme/css/chrome.css, giving it the value "Open Sans", sans-serif;, which is the default font used by the themes. This prevents us from interfering with the other themes' font, and has the positive side effect of allowing us to only use our pretty Serif font on the pages of the book. This means that the sidebar navigation will retain its "Open Sans" or fallback sans-serif font. I did this so that the website bits look like a website and the book bits look like a page in a book.

To do this, locate the .sidebar css class selector and add the property definition font-family: "Open Sans", sans-serif;, so that the final property definition looks like this:

.sidebar {
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    width: var(--sidebar-width);
    font-size: 0.875em;
    box-sizing: border-box;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-y: contain;
    background-color: var(--sidebar-bg);
    color: var(--sidebar-fg);
    font-family: "Open Sans", sans-serif;
}

Next, in src/theme/css/general.css, we add add a css class selector definition with the title of our custom theme, and fill it with the values of any of the other themes definined in the file. I just copied the values contained in the .rust definition because it contained the closest color scheme to what I desired. You then, modify the values of each property to match what you're looking for. I found it's some guess and check work but mdbook serve recompiles your site every time you save a change, and VScode or other editor plugins make it easier by adding in-line color pickers and such. You then, add a variable named --font to the existing css class selector definitions of each theme, .ayu,.coal,.light,.navy, and .rust, and set its value to "Open Sans". Make sure to also include a --font variable in your custom theme. As mentioned before, I went with "Garamond Regular".

For brevity, we will only look at an example of a single modified theme located above our new custom theme definition in src/theme/css/general.css. Also, I found that the Garamond font I downloaded appeared much smaller than the "Open Sans" font, so to remedy this I defined another variable, --content-font-size and set it to 1.5em.

.rust {
    --bg: hsl(60, 9%, 87%);
    --fg: #262625;

    --sidebar-bg: #3b2e2a;
    --sidebar-fg: #c8c9db;
    --sidebar-non-existant: #505254;
    --sidebar-active: #e69f67;
    --sidebar-spacer: #45373a;

    --scrollbar: var(--sidebar-fg);

    --icons: #737480;
    --icons-hover: #262625;

    --links: #2b79a2;

    --inline-code-color: #6e6b5e;

    --theme-popup-bg: #e1e1db;
    --theme-popup-border: #b38f6b;
    --theme-hover: #99908a;

    --quote-bg: hsl(60, 5%, 75%);
    --quote-border: hsl(60, 5%, 70%);

    --table-border-color: hsl(60, 9%, 82%);
    --table-header-bg: #b3a497;
    --table-alternate-bg: hsl(60, 9%, 84%);

    --searchbar-border-color: #aaa;
    --searchbar-bg: #fafafa;
    --searchbar-fg: #000;
    --searchbar-shadow-color: #aaa;
    --searchresults-header-fg: #666;
    --searchresults-border-color: #888;
    --searchresults-li-bg: #dec2a2;
    --search-mark-bg: #e69f67;

    --font: "Open Sans"
}

.papier {
    --bg: hsl(45, 46%, 85%);
    --fg: #262625;

    --sidebar-bg: #3b2e2a;
    --sidebar-fg: #c8c9db;
    --sidebar-non-existant: #505254;
    --sidebar-active: #e69f67;
    --sidebar-spacer: #45373a;

    --scrollbar: var(--sidebar-fg);

    --icons: #737480;
    --icons-hover: #262625;

    --links: #434341;

    --inline-code-color: #6e6b5e;

    --theme-popup-bg: #e1e1db;
    --theme-popup-border: #b38f6b;
    --theme-hover: #99908a;

    --quote-bg: hsl(60, 5%, 75%);
    --quote-border: hsl(60, 5%, 70%);

    --table-border-color: hsl(60, 9%, 82%);
    --table-header-bg: #b3a497;
    --table-alternate-bg: hsl(60, 9%, 84%);

    --searchbar-border-color: #aaa;
    --searchbar-bg: #fafafa;
    --searchbar-fg: #000;
    --searchbar-shadow-color: #aaa;
    --searchresults-header-fg: #666;
    --searchresults-border-color: #888;
    --searchresults-li-bg: #dec2a2;
    --search-mark-bg: #e69f67;

    --font: "Garamond Regular";
    --content-font-size: 1.5em
}

We then modify the font-family property in src/theme/css/general.css under the html element selector to use our --font variable we created previously in src/theme/css/variables.css. We also modify the value of the property font-family contained in the .body class selector definition.

To do this locate the html element selector and modify the font-family value so that final definition looks as follows:

html {
    font-family: var(--font), sans-serif;
    color: var(--fg);
    background-color: var(--bg);
    text-size-adjust: none;
}

Then locate the body element selector and add a font-family definition that uses the --font variable we defined previously:

body {
    margin: 0;
    font-size: 1.6rem;
    font-family: var(--font);
    overflow-x: hidden;
}

Finally, locate the .content class selector and modify the font-size value so that the final definition uses the --content-font-size variable we defined previously:

.content {
    overflow-y: auto;
    padding: 0 15px;
    padding-bottom: 50px;
    font-size: var(--content-font-size);
}

The last file you will need to modify is src/theme/index.hbs. Add a list item for your custom theme to the unordered list that defines the theme menu items.

To do this locate the ul element with the id "theme-list" and add an li element containing a button with a handlebars definition that passes off the hard work to mdbook. The new menu definition should look like this:

<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
    <li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li>
    <li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
    <li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
    <li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
    <li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
    <li role="none"><button role="menuitem" class="theme" id="papier">{{ theme_option "Papier" }}</button></li>
</ul>

Above, you can see the line I added for my custom theme, "Papier". The two parts that need to change if you just copy one of the lines of html for another theme are the button id and the variable passed to theme_option.

Here are all of the completed changes, sans font file, if you would like to just copy my files and make your adjustments after: