Monospace font characters are not fixed width

3k Views Asked by At

I'm trying to align some characters to draw a box in html. I've picked a monospace font so that characters are aligned, and drew the box with an equal number of characters for each line.

pre {
  font-family: 'Roboto Mono';
  white-space: pre;
}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet">
<pre>
    ╭─────────────────────────────────────────────────────────────────────────╮
    │.........................................................................│
    │-------------------------------------------------------------------------│
    │                                                                         │
    ╰─────────────────────────────────────────────────────────────────────────╯                         
</pre>

When I render this in the browser though, the rightmost edge can sometimes be misaligned depending on the monospaced font used.

When I use Roboto Mono:

misaligned box roboto mono

When I use Space Mono:

misaligned box space mono

For some reason, my monospace characters aren't monospaced. Why is this happening, and how do I enforce monospace in order to align characters?

2

There are 2 best solutions below

3
Ouroborus On BEST ANSWER

tl;dr: Google's font CDN isn't serving the fonts in their fullness. Download the zip they offer and host the fonts yourself.


Google's font CDN doesn't seem to be providing the full character range of some fonts. For example, Google's @import is:

@import url('https://fonts.googleapis.com/css2?family=Fira+Code&display=swap');

Looking at the CSS inside:

/* cyrillic-ext */
@font-face {
  font-family: 'Fira Code';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/firacode/v14/uU9eCBsR6Z2vfE9aq3bL0fxyUs4tcw4W_D1sJV37Nv7g.woff2) format('woff2');
  unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
  font-family: 'Fira Code';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/firacode/v14/uU9eCBsR6Z2vfE9aq3bL0fxyUs4tcw4W_D1sJVT7Nv7g.woff2) format('woff2');
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* more chunks like this omitted */

So the font is broken up into several files and they use unicode-range to map glyphs to specific files.

These are all the ranges they provide in that file:

unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
unicode-range: U+1F00-1FFF;
unicode-range: U+0370-03FF;
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;

The range for the box drawing characters is U+2500-257F which doesn't show up in the above list.

I tried adding them in but, as far as I can tell, the served files do not include that range.

However, you can download the original font files from Google (as a zip of .ttf files). Converting one of those font files into a data url and using it does render the glyphs correctly:

pre {
  font-family: 'Fira Code stripped';
}
@font-face {
  font-family: 'Fira Code stripped';
  src: url('data:application/octet-stream;base64,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');
}
<pre>
╭────────────────────────────────────────────╮
│............................................│
│--------------------------------------------│
│                                            │
╰────────────────────────────────────────────╯
</pre>

NOTE: In order to fit this example into an answer, I've stripped the font down to just the 9 characters needed to render the sample and converted it to woff2. External testing with the full ttf file produces the same results.

I conclude that Google simply isn't serving the complete font via their CDN.

11
Justinas On

Go to Google Fonts and write your character used for border. There will be shown empty glyph, so that means font does not have glyphs for border (top and bottom) and browser uses fallback font.

enter image description here

So you have few options:

  1. Use only monospace font
  2. Find font that supports all of your characters
  3. Use CSS to actually draw elements and not "ASCII-art"

pre {
  font-family: monospace;
  white-space: pre;
}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet">
<pre>
    ╭─────────────────────────────────────────────────────────────────────────╮
    │.........................................................................│
    │-------------------------------------------------------------------------│
    │                                                                         │
    ╰─────────────────────────────────────────────────────────────────────────╯                         
</pre>