Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to imitate a monospace font with a variable-width font?

(Edit: I only need this for the 10 digits 0..9, can I hardcode the letter width for each one of the 10 digits in the CSS file?)

I have read CSS - Make sans-serif font imitate monospace font but the CSS rule letter-spacing doesn't seem to be enough:

enter image description here

How to imitate a monospace fixed font from a standard sans-serif font?

This doesn't work perfectly:

.text {
  font-family: sans-serif;
  letter-spacing: 10px;
}
<div class="text">
ABCDEFGHIJKLMNOPQR<br>
STUVWXYZ0123456789
</div>
like image 220
Basj Avatar asked Oct 29 '25 07:10

Basj


1 Answers

TL;DR: you can't emulate monospace fonts

but you can get tabular (monospaced) numbers if supported via CSS

  • In the long run: it is much easier/efficient to opt for a proper monospace font or a font providing tabular numbers in the first place
  • try css property font-variant-numeric: tabular-nums if you only need to get tabular numbers

All these hacks are actually way more expensive and "wobbly" with regards to a predictable cross-browser rendering. However let's summarize these hacks:

Hack 1: Wrap all characters in inline-block elements

Apart from changing the actual font metrics in a font editor and compiling a new font you could split up all letters into an array of <span> elements via javaScript.

emulateMonospace();

function emulateMonospace() {
  let monoWraps = document.querySelectorAll(".toMonospace");

  monoWraps.forEach(function(monoWrap, i) {
    //remove all "\n" linebreaks and replace br tags with "\n"
    monoWrap.innerHTML = monoWrap.innerHTML
      .replaceAll("\n", "")
      .replaceAll("<br>", "\n");
    let text = monoWrap.textContent;
    let letters = text.split("");


    //get font-size
    let style = window.getComputedStyle(monoWrap);
    let fontSize = parseFloat(style.fontSize);

    //find maximum letter width
    let widths = [];
    monoWrap.textContent = "";
    letters.forEach(function(letter) {
      let span = document.createElement("span");
      if (letter == "\n") {
        span = document.createElement("br");
      }
      if (letter == ' ') {
        span.innerHTML = '&nbsp;';
      } else {
        span.textContent = letter;
      }
      monoWrap.appendChild(span);
      let width = parseFloat(span.getBoundingClientRect().width);
      widths.push(width);
      span.classList.add("spanMono");
      span.classList.add("spanMono" + i);
    });

    monoWrap.classList.replace("variableWidth", "monoSpace");

    //get exact max width
    let maxWidth = Math.max(...widths);
    let maxEm = maxWidth / fontSize;
    let newStyle = document.createElement("style");
    document.head.appendChild(newStyle);
    newStyle.sheet.insertRule(`.spanMono${i} { width: ${maxEm}em }`, 0);
  });
}
body{
  font-family: sans-serif;
  font-size: 10vw;
  line-height: 1.2em;
  transition: 0.3s;
}

.monospaced{
    font-family: monospace;
}

.letterspacing{
  letter-spacing:0.3em;
}

.teko {
  font-family: "Teko", sans-serif;
}

.serif{
    font-family: "Georgia", serif;
}

.variableWidth {
  opacity: 0;
}

.monoSpace {
  opacity: 1;
}

.spanMono {
  display: inline-block;
  outline: 1px dotted #ccc;
  text-align: center;
  line-height:1em;
}
<link href="https://fonts.googleapis.com/css2?family=Teko:wght@300&display=swap" rel="stylesheet">

<h3 style="font-size:0.5em;">Proper monospace font</h3>
<div class="monospaced">
WiWi</br>
iWiW
</div>

<h3 style="font-size:0.5em;">Letterspacing can't emulate monospaced fonts!</h3>
<div class="letterspacing">
WiWi</br>
iWiW
</div>


<hr>

<h3 style="font-size:0.5em;">Text splitted up in spans</h3>

<div class="toMonospace variableWidth">
ABCDEFGHIJKLMNOPQR<br>
STUVWXYZ0123456789<br>
</div>

<div class="toMonospace variableWidth teko">
ABCDEFGHIJKLMNOPQR<br>
STUVWXYZ0123456789<br>
</div>

<div class="toMonospace variableWidth serif">
This is<br>
not a<br>
Monospace<br>
font!!!
</div>

Each character will be wrapped in a span with an inline-block display property.

Besides all characters are centered via text-align:center.

The above script will also compare the widths of all characters to set the largest width as the span width.

Admittedly, this is not very handy
but this approach might suffice for design/layout purposes and won't change the actual font files.

As illustrated in the snippet:
In monospace fonts the widest letters like "W" are designed in a kind of squeezed manner (not distorted) whereas the thinner ones like "i" are designed visually stretched (e.g by adding bottom serifs).

enter image description here
Fira Mono vs. Fira Sans – the mono style "i" resembles a "slab serif" – introducing serifs

So proper monospace fonts can't really be emulated.

Hack 2: Substitute only digits (not really a hack)

Quite a few web fonts also include tabular numerals (sharing the same width) which can be applied via CSS property

font-variant-numeric: tabular-nums

2.2 Apply additional font only to numbers

We can specify a unicode-range in the @font-face rule to apply a font only to numbers

body {
  font-size: 10vw;
  font-family: Arial;
}

h3{
font-family: sans-serif;
font-size:16px;
line-height:1.3em;
}

h4{
font-family: sans-serif;
font-size:12px;
line-height:1.3em;
}


@font-face {
  font-family: ArialStd;
  font-style: normal;
  font-weight: 400;
  src: local("Arial");
}

@font-face {
  font-family: Arial;
  font-style: normal;
  font-weight: 400;
  src: local("Arial");
}
/** numbers **/
@font-face {
  font-family: Arial;
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/firasans/v17/va9E4kDNxMZdWfMOD5Vvl4jL.woff2) format("woff2");
  unicode-range: U+30-39;
}

.arialStd{
  font-family: ArialStd;
}

.firaNums{
  font-family: Arial;
  font-weight: 400;
}

.tabNum {
  font-family: Arial;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}
<h3>Variable width numerals</h3>
<h4>No substitution</h4>
<p class="arialStd">
Arial 01111<br>
Arial 10000
</p>


<h3>Numbers replaced via unicode range - not tabular</h3>
<h4>Arial (numbers with Fira-Sans)</h4>
<p class="firaNums">
Arial 01111<br>
Arial 10000
</p>


<h3>Numbers replaced via unicode range - tabular</h3>
<h4>Arial (numbers by Fira-Sans)</h4>
<p class="tabNum">
Arial 01111<br>
Arial 10000
</p>

Hack 3: (very experimental!) text-transform: full-width

This property normalizes rendered character-widths to a uniform value. This feature is currently only supported by Firefox.

body {
  font-size: 10vw;
  font-family: Arial
}

.tabNum {
  font-weight: 400;
  text-transform: full-width;
}
<h3>Only supported in Firefox</h3>
<p class="tabNum">
  Arial 01111<br> Arial 1000
</p>

Why letter-spacing doesn't work

letter-spacing just evenly inserts whitespace between all letters (...hence the name).
It won't normalize characters/glyphs to have the same widths.
We would need a css property like letter-width which doesn't exist.
Most likely we'll never get a similar property as text nodes don't add selectable sub nodes for each character – this would be kind of an overkill.

like image 64
herrstrietzel Avatar answered Oct 31 '25 10:10

herrstrietzel



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!