Death to typewriters

Technical supplement

Marcin Wichary
Feb 9, 2015 · 8 min read

This is a technical counterpart to the article about details of Medium typography. If you haven’t read that one, you should start there.

Below are some of the technical (CSS, JavaScript, Closure) details in Medium’s typography and typesetting. Note: we use LESS, so you will see some light LESS features (mostly variables).

I. Typography is for everyone

Em dashes, en dashes, quotes, primes, ellipses, and other replacements

We do most of the automatic type replacements the moment you type, often taking the characters happening before into consideration. Here’s a full list of all the replacements we do.

For copy and paste, we serialize paste into a sequence of keystrokes, and replay it using the same rules as above.

What is causing problems is external circumstances, for example an open issue where Chrome’s spellcheck is suggesting bad quotation marks instead of good ones.

Hanging quotes

Unfortunately, the CSS property hanging-punctuation isn’t supported by any browser.

We detect whether a paragraph starts with an opening single quote or double quote, and mark it with a CSS class. We support these opening quotes so far:


Then, we use text-indent CSS property, with values measured to offset the width of the respective quotation mark. This needs to be done per font:

.graf--p.graf--startsWithSingleQuote { // Freight Text 400 values
text-indent: -0.24em;
.graf--p.graf--startsWithDoubleQuote {
text-indent: -0.43em;
.graf--h2.graf--startsWithSingleQuote, // Bernino Sans 700 values
.graf--h3.graf--startsWithSingleQuote {
text-indent: -0.28em;
.graf--h3.graf--startsWithDoubleQuote {
text-indent: -0.47em;
.graf--h4.graf--startsWithSingleQuote { // Bernino Sans 300 values
text-indent: -0.25em;
.graf--h4.graf--startsWithDoubleQuote {
text-indent: -0.37em;

Centered paragraphs and headlines should not be indented, since that would just throw them off center:

.graf--startsWithDoubleQuote[data-align="center"] {
text-indent: 0;

There are a number of other ways this could be achieved, but this works relatively nicely for both reading and, very importantly, also for writing.

However, it is problematic when the hanging quotation mark is kerned with the next character. (Ideally, we would just absolute-position, but that wouldn’t work well while editing.)

Image for post
Image for post

Non-breakable spaces

We define what punctuation characters should be followed by a non-breakable space, and what should be preceded by one:

var NBSP = goog.string.Unicode.NBSP

And then when we render paragraphs, we decide which plain spaces turn into non-breakable spaces with this:

text = text.replace(_.NBSP_PUNCTUATION_START, '$1' + NBSP)
.replace(_.NBSP_PUNCTUATION_END, NBSP + '$1')

The side effect of this method is that we can upgrade our rendering without having to change the underlying text.

II. Making type read well and look good

Font smoothing

As far as I understand, this is only necessary for Macs:

-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

Line height and tracking

Relatively straightforward:

line-height: 1.5;
letter-spacing: 0.01rem;

Kerning and ligatures

We had a few little issues with using optimizeLegibility, but we are using it overall:

text-rendering: optimizeLegibility;

Firefox needed to turn the ligatures on independently:

-moz-font-feature-settings: “liga” on;

In some places where optimizeLegibility is kicking our ass (things unnecessarily breaking to a new line even though there is room), we just override it back:

text-rendering: auto;

Using different fonts for specific glyphs

Single one so far, for ██████ on Mac OS (but see pilcrows below).

@font-face {
font-family: 'Cambria';
src: local('Arial'), local('Helvetica');


We are hiding everything not in the main content area, so that it doesn’t print by default. In many cases we will still have to use display: none to hide individual screen-only UI objects, since the below alone still reserves the space… but this is still better than any new UI elements “leaking” into print view if we don’t pay attention.

@media print {
body.template-flex-article * {
visibility: hidden;
body.template-flex-article .postContent,
body.template-flex-article .postContent * {
visibility: visible;

We are setting up top and bottom margins:

@media print {
@page {
margin-top: .75in;
margin-bottom: .75in;

We’re changing the width of the page to match the different font size:

@media print {
body.template-flex-article .layoutSingleColumn {
max-width: 4.95in;
margin: 0 auto;

We’re overriding the font colour to be black, plus ensure that orphans and widows don’t exist (2 below means we don’t display one line by itself if it begins or ends the page). Alas, Firefox and Safari don’t support this yet.

@media print {
body {
color: black;

We’re hiding both default underlines and our custom ones:

@media print {
.markup—pre-anchor {
text-decoration: none;
background: none;

We’re hiding actionable elements in the footer, and make sure it travels to a new page together, rather than being cut in the middle:

@media print {
.postFooter--simple {
page-break-inside: avoid;

III. Punctuation binds the words together

Bullet points and ordered lists

This is the style we use for lists:

.postList > li:before {
position: absolute;
display: inline-block;
box-sizing: border-box;

And here are our custom bullet points:

ul.postList > li:before {
padding-top: 6px;
padding-right: 15px;
font-size: @fontSize-base--post * 0.65;
content: '•';


We only apply it to articles with a specified language, to avoid drive-by hyphenation:

.postArticle[lang] .graf--p,
.postArticle[lang] .graf--blockquote {
-webkit-hyphens: auto;
-webkit-hyphenate-limit-before: 2;
-webkit-hyphenate-limit-after: 3;
-webkit-hyphenate-limit-lines: 2;

Then we specify the proper language for the article:

<article class='postArticle' lang='en'>


We wrote an article about designing the custom underlines. Here is how we define them in the codebase:

// Position of the underline for a certain font
@backgroundPosition—underlineSerif: 0.72;
@backgroundPosition—underlineSansSerif: 0.90;

The modifier ceil was picked specifically so our underlines looked good under certain circumstances (your mileage may vary).

// Underline under regular text
.tier-1 .markup--anchor {
text-decoration: none;
background-image: linear-gradient(to bottom, @color-transparentBlack 50%, @color-transparentBlackDark 50%);
background-repeat: repeat-x;
background-size: 2px floor(@fontSize-base--post * @width--underlineRatio);
.m-underlinePosition(@fontSize-base--post, @lineHeight-base--post, @backgroundPosition--underlineSerif);

We specify overrides for retina displays to have sharp 1-pixel underlines:

// Retina override

Possible improvement is to specify positions in ems (which we avoid in our codebase), so that browsers with minimal font size would still have proper underlines.

We only do custom underlines in the body copy, not in the UI — they are pretty expensive to maintain.


Pretty straightforward styling of the pilcrow itself:

.pilcrow {
font-family: "Arial", sans-serif;
font-size: .7em;
padding: 0 .25em;
position: relative;
top: -.15em;
opacity: .4;

And we wrap it around where it appears:

text = text.replace(/¶/g, '<span class=”pilcrow”>¶</span>')

IV. Typography is more than just letters

Old-style numerals

We are lucky enough that the fonts we use come with proper defaults. Freight Text Pro (serif font you’re reading now) has default old-style numerals built in. Bernino Sans (sans serif headline font we use) has lining numerals.

Tabular figures

There is a CSS property (tabular-nums) living under font-variant. We cannot use it since the font we’re serving doesn’t support this OpenType property yet.

We fake it by wrapping each individual digit and comma with specially styled wrappers:

.tabularNumeral {
display: inline-block;
width: .56em;
text-align: center;
.tabularNumeral—comma {
width: .35em;
text-align: left;

And this is how we separate them: = function (numString) {
var tabularString = ''

Side note: This is what happened when I screwed up the above function:

Image for post
Image for post

Unicode is fun!

Thousand separators

This is a quick function that puts a comma every third number: = function (numString) {
return numString.replace(/\B(?=(\d{3})+(?!\d))/g, ",")

(You might notice a lot of these typographical enhancements are implemented as Closure templates compiler plugins, which do a lot to protect us from XSS security holes.)

V. Whitespace is as important as content

Multiple spaces

Multiple spaces is less important in HTML (browsers de-dup them), but we still need to remove them for when we render on other platforms, for example iOS.

Vertical spaces

At writing, we apply special class .graf--empty to a paragraph without any contents. Then we tighten things up with negative margins:

.graf--h2 + .graf--p.graf--empty,
.graf--h3 + .graf--p.graf--empty,
.graf--h4 + .graf--p.graf--empty {
margin-bottom: -7px;
margin-top: -7px;

Side note: why graf and not paragraph? Read on

Headline alignment

Very simple. Could conceivably be better expressed with ems as units.

.m-fontSizeWithLeftAlignmentFix(@size) {
font-size: @size;
margin-left: -@size / 20;

I personally implemented only a fraction of the above. Thank you to our tireless engineers who spend time caring about words and typography. In no particular order: Nick Santos (with extra thanks for reviewing this article), Daryl “Koop” Koopersmith, Jacob “Fat” Thornton, Kyle Hardgrave, and Gianni Chen.

« Go back to the main article

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store