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

1.6k Views Asked by At

(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>

4

There are 4 best solutions below

0
herrstrietzel On BEST ANSWER

TL;DR: imitating monospace fonts always introduces issues

  • 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 desined in a kind of squeezed manner (not distorted) whereas the thinner ones like "i" are designed visually stretched (e.g by adding bottom serifs).

So proper monospace fonts can't really be emulated.

Hack 2: Substitute only digits

Quite a few web fonts also include tabular numerals which can be applied via CSS property
font-variant-numeric: tabular-nums.

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;
}

@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;
}
.tabNum {
  font-family: Arial, ArialNum;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
}
<p>
Arial 01111<br>
Arial 1000
</p>

<p class="tabNum">
Arial 01111<br>
Arial 1000
</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.

2
emilsteen On

I've spent too much time trying to find a good monospaced font that works with several alphabets and had the look I wanted. These are the solutions I found (in order of recommendation):

  1. Find a monospaced font that you like and use that.
  2. Use a font editor and change all the letters to the same width (there might be a Python program that can do that, but I haven't tried it). But changing a font to monospaced will not look as good, there are a lots of craftsmanship in creating each letter so it will fit properly in the monospaced box.
  3. Use letter-spacing to simulate a simple monospaced font.
0
Temani Afif On

A very hacky solution. I repeat, a very hacky solution.

You can extend the text-shadow declaration to account for more letters if you want. You only need to manually adjust the variable --h based on your font. Don't rely on default fonts because it won't work across browsers.

The below works fine for me on Chrome Linux (and will break elsewhere)

.text {
  --w: 1.3em;  /* control the width */
  --h: 17px;   /* the height (adjust for your particular case) */

  font-family: sans-serif;
  writing-mode: vertical-lr;
  text-orientation: upright;
  height: var(--h);
  width: calc(14*var(--w)); /* 14 = number of shadows + 1 */
  overflow: hidden;
  text-shadow:
    calc(1*var(--w)) calc(-1*var(--h)),
    calc(2*var(--w)) calc(-2*var(--h)),
    calc(3*var(--w)) calc(-3*var(--h)),
    calc(4*var(--w)) calc(-4*var(--h)),
    calc(5*var(--w)) calc(-5*var(--h)),
    calc(6*var(--w)) calc(-6*var(--h)),
    calc(7*var(--w)) calc(-7*var(--h)),
    calc(8*var(--w)) calc(-8*var(--h)),
    calc(9*var(--w)) calc(-9*var(--h)),
    calc(10*var(--w)) calc(-10*var(--h)),
    calc(11*var(--w)) calc(-11*var(--h)),
    calc(12*var(--w)) calc(-12*var(--h)),
    calc(13*var(--w)) calc(-13*var(--h))
}
<div class="text">ABCDEFGHIJKLMNOPQR</div>
<div class="text">STUVWXYZ0123456789</div>

1
Java Wizard On

this works as a workaround to using spans it uses the css ruby display type.
it does the job by seperating each letter with a space also works best if each line is of same length i included a script that ensures this a bit late i know but i hope it helps!

    const doitElements = document.querySelectorAll(".doit");

    // 2. Iterate through each element 
      doitElements.forEach(element => {
      let content = element.textContent;

      // 3. Split the content into lines
      const lines = content.split('\n');

      // 4. Process each line
      const modifiedLines = lines.map(line => {
        // Space out individual letters
        return line.split('').join(' ');
      });

      // 5. Find the line with the maximum length
      let maxLength = 0;
      modifiedLines.forEach(line => {
        maxLength = Math.max(maxLength, line.length);
      });
      // 6. Pad shorter lines with spaces
      const paddedLines = modifiedLines.map(line => {
        let difference = maxLength - line.length;
        if (difference == maxLength || difference == 0)
        return line;
        difference = difference / 2 - 1;
       // debugger;
        return line + ' ⠀'.repeat(difference);
      });
      // 7. Construct the new HTML content
      const newContent = paddedLines.join('<br>');
      // 8. Update the original element's HTML
      element.innerHTML = newContent;
     });
     div {
      font-family: Arial, Helvetica, sans-serif; /* Your desired 
      font */
      display: ruby;/* the code that makes it work*/
      letter-spacing: 0.2em; /* Adjust this value */
      word-spacing: -0.4em;  /* Adjust this value */

    } 
<h4> when text has different length does not work perfectly</h4>
<div class="doit">
ABCDEFGHIJKLMNOPQRasdasd<br>
STUVWXYZ0123456789
</div>
<h4>when it has the same length it works kinda</h4>
<div class="doit">ABCDEFGHIJKLMNOPQR<br>
STUVWXYZ0123456789</div>
<h4>or when adding spaces manually also kinda works</h4>
<div class="without_js">A B C D E F G H I J K L M N O P Q R<br>S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9</div>

Disclaimer: this only ensures that the text has the same length visually linke in Word or any writing program the Block type text formatting does this results in the text not lining up perfectly in the middle so the initial problem perseveres.

Another better solution is this:

const doitElements = document.querySelectorAll(".doit rb");

    // 2. Iterate through each element 
      doitElements.forEach(element => {
      let content = element.textContent;

      // 3. Split the content into lines
      const lines = content.split('\n');

      // 4. Process each line
      const modifiedLines = lines.map(line => {
        // Space out individual letters
        return line.split('').join(' ');
      });

      // 5. Find the line with the maximum length
      let maxLength = 0;
      modifiedLines.forEach(line => {
        maxLength = Math.max(maxLength, line.length);
      });
      // 6. Pad shorter lines with spaces
      const paddedLines = modifiedLines.map(line => {
        let difference = maxLength - line.length;
        if (difference == maxLength || difference == 0)
        return line;
        difference = difference / 2 - 1;
       // debugger;
        return line + ' ⠀'.repeat(difference);
      });
      // 7. Construct the new HTML content
      const newContent = paddedLines.join('</rb><br><rb>');
      // 8. Update the original element's HTML
      element.innerHTML = newContent;
     });
ruby {
 font:1em;
  font-family: Arial, Helvetica, sans-serif;
  ruby-position:border;
  display:ruby-text;
  width: 261px;
  word-spacing: 0em;
}
.doit,.second {
 width: 215px !important;
}
<h3>solution pure css</h3>
<ruby>
  <rb>T h i s i s a l o n g t e x t t o c h e c k</rb><br>
  <rb>T h i s i s a 8 8 8 8 t e x t t o c h e c k</rb>
</ruby>

<h3>Letters spaced with js:</h3>
<ruby class="doit">
<rb>
ABcdeFGhijklMNop<br>
QrstuVWXyZ123973
</rb>
</ruby>
<ruby class= "second">
<rb>A B c d e F G h i j k l M N o p</rb><br>
<rb>Q r s t u V W X y Z 1 2 3 9 7 3</rb>
</ruby>

But still overall it would be best to choose a monospace font from a site like google fonts tbh there is no merit in seeking another solution other than a monospace font, as eediting a font to be monospace is also possible (look at the free tool glyphr studio there it has an option to convert any imported font to monospace)(picture: )monospace on glyphr studio