A pixel font by David Hatley
Stem 7
A pixel font that refuses to blur. Set it at 16, 32, 48, 72 or 120 px and every edge lands on a whole pixel. Other sizes work too. They just look disappointed in you.
Try it
48 px · 6 px cells · click the text to edit it
It started as a logo
My mark is a d and an h sharing one stem, drawn on 7 × 7 cells. Its stem set the cap height, its bar set the x-height, and the rest of the alphabet had to fit around it. Type dh in Stem 7 and you get the mark back.
The mark · 7 × 7
Caps are 7 cells, the x-height is 4, the descender is 2. The bar of the mark sits on row 4, so every bar, dash, arrow and operator in the font sits there too.
Drawn in a text file
No font editor. Each glyph is nine rows of X and . in a plain text file, with the reason for the drawing written above it. Diffs are readable and every argument with myself is in version control.
The built font at 120 px
One CLI reads these files and builds the TTF and WOFF2. It also lints the drawings, derives the kerning, renders proofs and counts grey pixels.
Eight cells to the em
The em is 2,400 units, or 8 cells of 300. So one cell is always the font size divided by eight, and at the five house sizes that comes out to a whole number of pixels.
Font hinting is the usual fix for blurry pixel fonts. I tested it, and Chromium on Linux ignored the hints in every rendering mode, so the outlines do all the work themselves.
Zero grey pixels
A crisp pixel font has two colours: ink and not ink. Any pixel in between is the rasteriser guessing. This loupe renders the font in your browser right now and counts the guesses.
| Build | 16 | 32 | 48 | 72 | 120 |
|---|---|---|---|---|---|
| The first build (1,000 UPM) | 63.5% | 35.5% | 25.9% | 18.4% | 0% |
| Stem 7 1.001, Regular | 0 | 0 | 0 | 0 | 0 |
| Stem 7 1.001, Bold | 0 | 0 | 0 | 0 | 0 |
Grey pixels are marked in vermilion. The table comes from the build's own test, which renders every style at every size in headless Chromium and fails the build if one grey pixel shows up.
The one with gaps
Grid draws every cell as its own square, with a gap cut from the top and right. No single gap stays whole at every size, so Grid is a variable font: the gap is half a cell up to 40 px and a third of a cell from 48 px. Browsers pick it automatically.
At 16 and 32 px it is crisp but light, so it reads best from 48 px up.
Three rounds of arguing with myself
The first draft looked fine as an alphabet and fell apart in words. place read as ploce, and 64×64 had a lowercase x in the middle. Each fix below has its reason written into the source.
Grey is round 0, black is the shipped drawing. Most fixes came from setting real words and reading them at 1:1, not from staring at the alphabet.
Bold, by hand
The bold draft is a rule: double every column that carries a stem. That works for H and falls apart on diagonals, curves and dots. 64 of the 120 bold glyphs were redrawn by hand.
Lint now fails any Bold glyph with a stroke one cell thin, which is how the rule's misses were found.
Kerning in whole cells
Kerning comes from the drawings, not from my eyes at 2 a.m. For each pair, compare the facing edges row by row and average the gap. Under 3 cells: leave it. 3 to 5: pull in one cell. 5 or more: two. Never closer than one cell, and never into a mouth like C's.
Every character
Good at small, exact things
HUDs, counters, labels, numbers that change while you watch. Figures are tabular, so they don't jiggle. Not recommended for your novel.
$ stemctl crispRegular 16 0 grey PASSRegular 120 0 grey PASSall crisp
Get it
TTF and WOFF2 for Regular, Bold and Grid, a ready-made stem7.css and the licence. One zip, 30 KB. No account, no newsletter, no "pay what you want" guilt screen.
Desktop: open the TTF files and install. Web: use the WOFF2 files.
Install straight from GitHub. No registry account involved.
npm install github:davidghatley/stem7
import "stem7/stem7.css";
<h1 class="stem7 stem7--48">Hello</h1>
Served from GitHub by jsDelivr. Paste it into your <head> and move on with your life.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/davidghatley/stem7@v1.0.1/stem7.css">
@font-face {
font-family: "Stem 7";
src: url("https://cdn.jsdelivr.net/gh/davidghatley/stem7@v1.0.1/fonts/Stem7-Regular.woff2") format("woff2");
}
.pixel {
font-family: "Stem 7";
font-size: 32px; /* 16, 32, 48, 72 or 120 */
line-height: 1;
-webkit-font-smoothing: none;
}- Five sizes. 16, 32, 48, 72, 120 px. Anything else is a blur with extra steps.
line-height: 1. The baseline then sits exactly 7 cells below the top of the line, at every size.- Caps and figures at 16 px. Lowercase there has an x-height of 8 pixels. It tries its best. Start lowercase at 32.
- Smoothing off.
-webkit-font-smoothing: noneon macOS. The font has nothing to smooth. - Whole-pixel positions. Half a pixel of offset buys you all of the blur.
- Grid from 48 px up. Smaller works, but it gets thin.
Changelog and licence
| Version | What changed |
|---|---|
| 1.001 | F now kerns against a, c, e, g, m, n, o, r, u, w and y. The source asked for it all along; the build quietly dropped those 11 pairs. A new test shapes every pair with HarfBuzz so it can't happen again. |
| 1.000 | Regular, hand-drawn Bold and variable Grid. 0 grey pixels at all five sizes. |
Free for anything, commercial included, under the SIL Open Font License 1.1. There is no reserved font name, so subset it, convert it, rename it. Just don't sell the font files on their own.