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

I have read CSS - Make sans-serif font imitate monospace font but the CSS rule letter-spacing doesn't seem to be enough:
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>

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.
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 = ' ';
} 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" get squeezed (not distorted)
whereas the thinner ones like
"i" get visually stretched (e.g by adding bottom serifs).
So proper monospace fonts are completely different and can't really be emulated.

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):
Find a monospaced font that you like and use that.
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.
Use letter-spacing to simulate a simple monospaced font.

Related

What unicode spaces are equal to letter widths?

I am trying to replace some letters of a text (in a non monospace font) with spaces of same width.
Is it possible? How?
Are there any non-monospace fonts that allow this?
I found these unicode spaces, and apparently U+2002 is an EN SPACE and U+2003 an EM SPACE.
But the widths don't always match.
<p>the en space looks nice</p>
<p>the e  space looks  ice</p>
<p>but em space has more width</p>
<p>but e  space has  ore width</p>
And I wonder what spaces should I use for other letters...
I know I could set transparency or color to individual letters. But I wanted to find an easy solution with spaces.
Your question was interesting so I thought about the fact that you wanted a real space instead of the missing char, typically for a copy-paste operation. Using CSS pseudo-elements could be a solution because the content of them isn't used for text selection.
The space chars have a fixed width, which is not the case for letters since a m is wider than a i. I think that EM space is 1 em wide and EN space is 0.5 em.
So my idea was to replace your char, let's say m, by this:
<span class="pseudo-space" data-char="m"><span> </span></span>
Yes, this is a lot of HTML and you are asking yourself "why having an inner span?". Well, the problem is that we want the space to have no width and we want the pseudo-element to take this space instead. I also noticed that if you replace the non-breaking space by the normal space then you get no space at all when you select the text for a copy-paste.
I came out to this solution:
span.pseudo-space {
position: relative; /* Child span will be absolute. */
overflow: hidden; /* Just to avoid a potential scrollbar. */
}
span.pseudo-space > span {
position: absolute; /* Avoid width calculation on the parent span. */
}
span.pseudo-space::after {
content: attr(data-char); /* Display the original char. */
color: transparent;
}
/* Just to have a gray block to see the text aligned. */
blockquote {
margin: 0.5em 0;
padding: 1em;
background: #eee;
display: inline-block;
}
blockquote p:first-child {
margin-top: 0;
}
blockquote p:last-child {
margin-bottom: 0;
}
<blockquote>
<p>The "n" character will never be the same width as a space character.</p>
<p>The "<span class="pseudo-space" data-char="n"><span> </span></span>" character will <span class="pseudo-space" data-char="n"><span> </span></span>ever be the same width as a space character.</p>
<p>The "m" character will never be the same width as a space character.</p>
<p>The "<span class="pseudo-space" data-char="m"><span> </span></span>" character will never be the sa<span class="pseudo-space" data-char="m"><span> </span></span>e width as a space character.</p>
</blockquote>
Try to copy-paste the content of the blockquote and you should normally get the space instead of the hidden char. (Tested on Chrome, Firefox and Edge).
I wanted to do something like this.
If I could replace letters with spaces of the same width, it would be much easier. I wouldn't need to play with HTML elements and style.
// Convert letter to spans with opacity:0
const p = document.querySelector("p");
p.innerHTML = p.innerText
.split("")
.map(c => `<span class='off'>${c}</span>`)
.join("");
const letterSpans = p.querySelectorAll('span');
// Create random sequence of indexes
const indexes = Array(letterSpans.length).fill().map((e, i) => i)
indexes.sort((a, b) => 0.5 - Math.random());
// Display letters in the order indicated by `indexes`
function displayLetterAtIndex(i) {
letterSpans[indexes[i]].classList = "on";
if (i < letterSpans.length - 1) {
setTimeout(() => displayLetterAtIndex(i+1), 50);
}
}
displayLetterAtIndex(0);
.on {
opacity: 1;
}
.off {
opacity: 0;
}
span {
transition: opacity 1s linear;
}
<p>Letters will appear in a random order</p>

How do I make the width of each character the same?

I'm using a custom font. The design doesn't allow me to change font.
While the number change, the content after it got pushed around due to the different width of digits.
Is there a way to make all the digits same width? I don't want to assign the width of the span component because I need this to be inline and the width should be determined by the number of digits it has.
const numberDom = document.querySelector('.number');
let number = 0;
function tick() {
number += 1;
numberDom.innerText = number;
}
setInterval(tick, 50);
p {
font-size: 1rem;
}
.number {
font-family: 'Carter One', cursive;
font-size: 1.5rem;
color: #365;
}
.number::after {
content: 'pt';
font-size: 0.75em;
margin-left: 0.1em;
color: #587;
}
<link href="https://fonts.googleapis.com/css?family=Carter+One&display=swap" rel="stylesheet">
<p>You got <span class="number"></span>, good job!</p>
Okay so first I made a function that it changes the looks of your <p>. after that the only solution I could find to stop the jiggling was to put the number in an inline-block.
Here is my fiddle
Edit
I made a script that changes the width of the inline-block. It's an if statement that if your number is 1000 or higher the width will change.
Fiddle
You can always make an else if with over 10000 et cetera.
Change your number class as:
.number {
font-family: 'Carter One', cursive;
font-size: 1.5rem;
color: #365;
width: 56px;
display: inline-flex;
}
You can use
.numbers{
font-family: Tahoma;
}
It will fix the number jumping/height abnormality issue.

Difference in pixel for same length of characters

While working in css I had faced one issue.
The character with same number of length has difference in pixel values.
<html>
<div>
<p><b>WWWWW</b></p>
<p><b>IIIII</b></p>
<p><b>AAAAA</b></p>
</div>
</html>
WWWWW
IIIII
AAAAA
Both has same [5] character length.
How we can adjust them in CSS?
You can use font-family: monospace;
This will use a font that reserves the same horizontal space for each letter, regardless of their actual width
* {
font-family: monospace;
}
<div>
<p><b>WWWWW</b></p>
<p><b>IIIII</b></p>
<p><b>AAAAA</b></p>
</div>
In my example that font will be applied to all text, but you can of course restrict it to particular element/s by using a tag, class or ID selector (or any combined selector you come up with) instead of the * selector in the CSS rule I used.
You can't do it in CSS, the maximum you can specify is font-family: monospace. Here you have a JS solution.
HTML:
<div class="scaled">WWWWWWWWWW</div>
<div class="scaled">IIIIIIIIII</div>
<div class="scaled">AAAAAAAAAA</div>
CSS:
.scaled > span {
display: inline-block;
width: 20px;
border: 1px solid lightgray;
text-align: center;
}
JAVASCRIPT:
$('.scaled').each(function(){
let $scaled = $(this);
let chars = $scaled.text().split('');
let charsSpans = chars.map((char) => $('<span>' + char + '</span>'));
$scaled
.empty()
.append(charsSpans);
});

How can I scale arbitrary text to always fit the viewport width?

A site I'm busy working on has a section with some very large headings. There's something I'm not sure how to handle:
The heading may be one two short or long words, e.g: "Cyprus" to "Nouvelle Zelande", and it must scale to be roughly the width of the viewport. That means "Cyprus", being shorter, will have larger individual characters than longer text than "Nouvelle Zelande".
This would be relatively easy to do with JavaScript, I think, but I'd like to go for a pure HTML/CSS solution. So: how can I scale text to fit the width of the viewport? So far, I'm stumped and not sure how to do it, myself.
Some details:
You only need to target the most recent version of each browser, which includes IE11.
You may use any and all HTML5 and CSS3 that works within those browsers.
It's okay if you make the text "Nouvelle Zelande" word-wrap, as long as the longer of the two words still roughly fits to the width available.
You may add extra elements inside/around the headings.
Note that viewport units are not a solution. Previous questions asking about this (Pure CSS to make font-size responsive based on dynamic amount of characters, Font scaling based on width of container) have answers of "use viewport units, like vw!", but that doesn't handle this scenario at all, and astute readers even pointed this out. I've even used vw in the code sample below to demonstrate its non-solution-ness. It'll size based on the viewport just fine, but won't do any sizing based on the amount of text.
Code sample
h2 {
font-family: sans-serif;
text-transform: uppercase;
font-size: 14vw;
white-space: nowrap;
overflow-x: hidden;
margin: 0;
}
<h2>Nouvelle Zelande</h2>
<h2>Australia</h2>
<h2>Cyprus</h2>
The only unit, if being used to set font size, that is relative to the size of its container, is viewport units vw/vh, which will not solve your case alone, even if the container is the same width as the viewport, since it does not calc the letter size to fit into the container.
The closest non-script solution I can come up with is to use the CSS element counter trick, and wrap each letter in a span
The 130vw I set here, worked best for the given font, though this might need to be adjusted based on which font family is being used.
h2 {
display: inline-block;
font-family: sans-serif;
text-transform: uppercase;
white-space: nowrap;
overflow-x: hidden;
margin: 0;
}
/* 1 letter */
h2 span:first-child:nth-last-child(1) {
font-size: 130vw;
}
/* skipped 2-5 in this demo */
/* 6 letters */
h2 span:first-child:nth-last-child(6),
h2 span:first-child:nth-last-child(6) ~ span {
font-size: calc(130vw / 6);
}
/* skipped 7-14 in this demo */
/* 15 letters */
h2 span:first-child:nth-last-child(15),
h2 span:first-child:nth-last-child(15) ~ span {
font-size: calc(130vw / 15);
}
<h2><span>N</span><span>o</span><span>u</span><span>v</span><span>e</span><span>l</span><span>l</span><span>e</span> <span>Z</span><span>e</span><span>l</span><span>a</span><span>n</span><span>d</span><span>e</span></h2><br>
<h2><span>C</span><span>y</span><span>p</span><span>r</span><span>u</span><span>s</span></h2>
Here is the same concept using a script, and without the span's
(function (d,t) {
window.addEventListener("resize", throttler, false);
window.addEventListener("load", throttler(), false); /* run once on load to init */
function throttler() {
if ( !t ) {
t = setTimeout(function() {
t = null;
keepTextFit(d.querySelectorAll('h2'));
}, 66);
}
}
function keepTextFit(el) {
var f = el[0].getAttribute("data-font");
for (var i = 0; i < el.length; i++) {
var c = el[i].textContent.split('').length;
el[i].style.cssText =
'font-size: calc(' + f + ' / ' + c + ')';
}
}
})(document,null);
h2 {
display: inline-block;
font-family: sans-serif;
text-transform: uppercase;
white-space: nowrap;
overflow-x: hidden;
margin: 0;
}
<h2 data-font="130vw">Nouvelle Zelande</h2>
<h2>Australia</h2>
<h2>Cyprus</h2>
Note, since resize events can fire at a high rate, the throttler is used to reduced the rate so the handler doesn't execute expensive operations such as DOM modifications too often.
If you want to make a perfect fit, check this post: fit-text-perfectly-inside-a-div
If you are looking to use a plugin there's
http://fittextjs.com/
wich can do that for you

Text changes height after adding unicode character

I have HTML element with text content.
Font is set to sans-serif in CSS.
Text is updated via JavaScript.
Sometimes contains just ASCII characters but, sometimes, includes "➜" character. See following snippet:
var text = document.getElementById("text");
var chars = "A➜";
var i = 0;
function update() {
i=1-i;
text.innerText = "char: " + chars[i];
setTimeout(update, 500);
}
update();
div {
font-family: sans-serif;
background-color: lightgrey;
}
<div id="text" />
This works fine in IE11 but in Chrome the element "wiggles":
It looks like this happens because different font is used to render "➜" character:
Arial—Local file(5 glyphs)
Segoe UI Symbol—Local file(1 glyph)
Is there a simple way to stabilize the height of whole element and position of static part of text?
One way seems to be using "Segoe UI Symbol" for whole element - but I prefer a different font for regular text.
Just add a line-height style to your element:
var text = document.getElementById("text");
var chars = "A➜";
var i = 0;
function update() {
i=1-i;
text.innerText = "char: " + chars[i];
setTimeout(update, 500);
}
update();
div {
font-family: sans-serif;
background-color: lightgrey;
line-height: 1em;
}
<div id="text" />
An easy fix would be to set the line-height in CSS
var text = document.getElementById("x");
var chars = "A➜";
var i = 0;
function update() {
i=1-i;
text.innerText = chars[i];
setTimeout(update, 500);
}
update();
div {
font-family: sans-serif;
background-color: lightgrey;
line-height: 1em;
}
#x {
line-height: 1em;
}
<div id="text">char: <span id="x" /></div>