Typography is more than just what fonts you use. Typography is everything that has to do with how text looks – such as font size, line length, color, and even more subtle things like the whitespace around a text. Good typography sets the tone of your written message and helps to reinforce its meaning and context.
This is a handbook on best typographic practices through the lens of a web designer. It is meant to be short and concise – used as a reference rather than explanatory. For in-depth explanations and details, look at the links in the Further Readings of each section instead.
Lastly, this handbook is open source on GitHub and will continuously be updated with the best practices. Enjoy.
Visual hierarchy is the concept of organizing elements on a page in a way that establishes an order of importance, allowing readers to easily navigate the page and find relevant content. When done correctly, it should guide the reader's eye throughout different sections of the page. Visual hierarchy is very prevalent in typography, and forms the basis of typographic design theory.
Consider the following typographic design of Alice's Adventures In Wonderland, which clearly establishes a visual hierarchy:
Visual hierarchy can be broken down into 5 different parts:
Gestalt principles, or gestalt laws, are rules of the organization of perceptual scenes. When we look at the world, we usually perceive complex scenes composed of many groups of objects on some background, with the objects themselves consisting of parts, which may be composed of smaller parts, etc. Scholarpedia
The two most important Gestalt Laws to understand in typography are the Law of Proximity and the Law of Similarity.
To master the positioning of elements, it is crucial to understand the Gestalt Law of Proximity. The Law of Proximity states that humans perceive objects that are closer together as related objects. And vice versa, objects that are farther apart are perceived as different groups.
In typographic design, "proximity" refers to the amount of whitespace created by line height, margin, and padding. There should be a distinctive and noticeable amount of additional whitespace between two different sections. Consider the following example:
Note that the Law of Proximity does not mean that you should squeeze related content in a small container. No, free flowing whitespace is important too. Simply add noticeable, additional whitespace between two separate sections.
The Gestalt Law of Similarity states that entities that look similar tend to be grouped together. For example, if all clickable texts are sky-blue, the audience will assume that all textual content that is sky-blue is clickable.
In typography, the law of similarity just means to keep your styles consistent on elements that serve the same function. Group of elements should also look similar to other groups that serve the same function, for example: the visual styles of one blog post should look similar to another blog post. On the other hand, elements with a different functions should not look similar to one another.
Choosing fonts is a creative and emotional process. Different fonts convey different feelings, and you want a font that complements the tone of your text.
To declare custom web fonts, use the following syntax:
@font-face {
font-family: 'Helvetica Neue';
src: url('/assets/fonts/HelveticaNeue-Light.eot');
src: url('/assets/fonts/HelveticaNeue-Light.eot?#iefix') format('embedded-opentype'),
url('/assets/fonts/HelveticaNeue-Light.woff2') format('woff2'),
url('/assets/fonts/HelveticaNeue-Light.woff') format('woff'),
url('/assets/fonts/HelveticaNeue-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Helvetica Neue';
src: url('/assets/fonts/HelveticaNeue-Bold.eot');
src: url('/assets/fonts/HelveticaNeue-Bold.eot?#iefix') format('embedded-opentype'),
url('/assets/fonts/HelveticaNeue-Bold.woff2') format('woff2'),
url('/assets/fonts/HelveticaNeue-Bold.woff') format('woff'),
url('/assets/fonts/HelveticaNeue-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Helvetica Neue';
src: url('/assets/fonts/HelveticaNeue.eot');
src: url('/assets/fonts/HelveticaNeue.eot?#iefix') format('embedded-opentype'),
url('/assets/fonts/HelveticaNeue.woff2') format('woff2'),
url('/assets/fonts/HelveticaNeue.woff') format('woff'),
url('/assets/fonts/HelveticaNeue.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
font-family
, instead of having a different font-family
name for each type-family.Alternatively, you can also import fonts using an online web font service, such as Google Fonts or Typekit.
Before custom fonts are displayed, they need to be loaded first. There are three possible scenarios for font loading:
Scenario 1 only happens when you try to use a nonexisting font, or a declaration with a bad src
. This can and should be avoided entirely. Jumping to Scenario 3, it is the best case scenario and can usually be achieved through proper font caching. Scenario 2 is the scenario that involves font loading. Font loading is mostly unavoidable (at least for the first request instance). There are several ways to deal with it:
1. Flash of Unstyled Text (FOUT). A FOUT is an instance where a web page uses default and fallback fonts before switching to the proper web font. It happens because font requests do not happen until both HTML and CSS are downloaded. This means that there is a period of time where HTML is displayed before fonts are fully downloaded. The FOUT is the optimal approach for most websites, mainly because the alternatives are a lot worse. When done correctly, a FOUT is hardly noticeable.
2. Flash of Invisible Text (FOIT). A number of years ago, some modern browsers started to implement a new technique of dealing with font loading — the FOIT. A FOIT is an instance when the browser detects that a font is currently loading, and hides the text until font loading is complete. There is usually a maximum wait time before the browser switches to a fallback. This approach should always be avoided. Although it might sound good in theory, it can provide an awful user experience for people with slower internet. It can cause a FOUT after the initial FOIT, and at worst can even lead to permanent invisible content.
3. The Whitescreen Approach. The entire web page does not display until fonts are loaded. Alternatively, a loading progress bar can be displayed. This approach is ONLY recommended if a FOUT is going to heavily detract from the user experience of your audience. This is usually the case if the web page heavily relies on very distinct fonts in large sizes. Otherwise, a FOUT is preferred because content is king. This approach is similar to FOIT, but superior because you control when to start showing content instead of the browser. In FOIT, invisible text might also confuse the audience, whereas a completely white screen (or a progress bar) is an obvious sign of loading.
Whether you plan on going with the FOUT approach or the Whitescreen approach, you will want to use a JavaScript library called Web Font Loader. Web Font Loader gives you added control over @font-face, and adds events for you can control the font loading experience.
Note: There is a W3C Font Loading API that achieves similar goals, but its support is still poor.
The easiest way to font-load with the FOUT approach is by making use of the recently added font-display
CSS property. font-display
instructs the browser how the font should be displayed while it is in the downloading state, loading state, or ready state. The value that we want for the FOUT approach is font-display: swap
. This tells the browser to render a fallback font until the main font is ready.
@font-face {
font-family: 'Helvetica Neue';
src: url('/assets/fonts/HelveticaNeue-Light.eot');
font-display: swap;
}
As font-display
is still relatively new, browser support for it is limited. Check here to see if your target browsers support it.
A more widely-supported method of achieving FOUT is using JavaScript to append a CSS class once font is ready. Here is an example of this done using Web Font Loader:
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
<script type="text/javascript">
WebFontConfig = {
google: { families: [ 'Lora' ] }
};
</script>
<style>
p {
font-family: "Arial";
}
.wf-active p {
font-family: "Lora, Arial";
}
</style>
In the above code, the browser first renders paragraph texts with Arial. Meanwhile, the Web Font Loader library starts loading Lora from Google Fonts. Once it is loaded, the wf-active
class is appended to the html
element, and the Lora font starts to be used for paragraph texts.
Here is an example of using Web Font Loader with the whitescreen approach:
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.16/webfont.js"></script>
<script>
WebFont.load({
google: {
families: ['Raleway', 'Oswald']
}
});
</script>
<noscript>
<link href='https://fonts.googleapis.com/css?family=Raleway|Oswald' rel='stylesheet' type='text/css'>
</noscript>
<style>
.wf-loading {
display: none;
}
.wf-active p {
font-family: "Arial";
}
p {
font-family: "Raleway, Arial";
}
</style>
OpenType features can be thought of as typographic options for the font. They can be used to enhance the legibility and appearance of text.
p {
font-kerning: normal;
font-variant-ligatures: common-ligatures contextual;
font-feature-settings: "kern", "liga", "clig", "calt";
}
font-feature-settings
to enable OpenType features.kern
, ligatures liga
, contextual ligatures clig
, and contextual alternatives calt
should always be enabled for all texts.Use relative units whenever possible.
html { font-size: 100% }
p { font-size: 1em }
@media (min-width: 64em) {
html {
font-size: 112.5%;
}
}
font-size: 100%
listens to the browser’s font size settings instead of overwriting it. By default in most browsers, this would place 1em
at 16px
.font-size
of the html
will also affect every em
and rem
element. This can be useful for implementing responsive web design.font-size: 100%
and 1em
.rem
and em
for font-size
.rem
, em
, or %
for element positioning (margin
, padding
, etc).em
for media query dimensions.vw
and vh
due to incomplete support, difficulty in precise configuration, and because it does not listen to browser font or zoom settings.The container, also known as the wrapper, is an HTML element that encloses one or more other elements. It allows for grouping elements semantically, cosmetically, or in layout.
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
.container {
max-width: 67rem;
padding-left: 1.5rem;
padding-right: 1.5rem;
}
box-sizing: border-box
. Click here for more information about it.max-width
with left and right padding
is an easy way to create a mobile-friendly container.Use a modular scale to help you decide on the font-size
of your elements. Modular scale refers to a series of harmonious numbers that relate to one another in a meaningful way.
A single modular scale will rarely look good on all resolutions. To remedy this, different scales can be used depending on the resolution of the viewer's device.
//Sass responsive modular scale
/*
* Modular scale
* http://www.modularscale.com/?1.25&em&1.33&web&text
*/
$type-scale-large: (
h1: 3.911rem,
h2: 2.941rem,
h3: 2.211rem,
h4: 1.663rem,
p: 1.25rem
);
/*
* Modular scale
* http://www.modularscale.com/?1.25&em&1.25&web&text
*/
$type-scale-medium: (
h1: 3.052rem,
h2: 2.441rem,
h3: 1.953em,
h4: 1.563rem,
p: 1.25rem,
);
/*
* Modular scale
* http://www.modularscale.com/?1.1&em&1.25&web&text
*/
$type-scale-small: (
h1: 2.686rem,
h2: 2.148rem,
h3: 1.719rem,
h4: 1.375rem,
p: 1.1rem
);
$breakpoint-medium: 75em;
$breakpoint-small: 45em;
@mixin size($level) {
font-size: map-get($type-scale-large, $level);
@media (max-width: $breakpoint-medium) {
font-size: map-get($type-scale-medium, $level);
}
@media (max-width: $breakpoint-small) {
font-size: map-get($type-scale-small, $level);
}
}
// Example
.title {
@include size(h1);
}
Vertical spacing is created by line-height
, margin
, and padding
.
line-height
should be unitless. Wide containers should have text with a larger line-height
, while narrow containers look better with text with a smaller line-height
.margin-bottom
.Vertical rhythm is the concept of keeping vertical spaces between elements consistent. It is incredibly important as it helps to create a visually relaxing experience, and evokes a feeling of familiarity to users.
Establishing a vertical rhythm is simple. First, decide on a base whitespace value that you will use for vertical margins and vertical padding. Then, apply this value as a single-direction margin (or padding) to your containers, textual elements, and other relevant elements. For larger gaps, use a multiple of the base value.
Setting the base vertical spacing to be the same size as the line-height
will allow every line to fit in an imaginary baseline grid. This is often done to imitate the uniformity of print design. This is not a requirement of vertical rhythm; any value works for base vertical spacing as long as a multiple of it is repeated consistently.
body {
line-height: 1.4; // Base line height
}
p {
font-size: 1.25em; // Base font size
margin-bottom: 1.75rem; // Base vertical spacing: (1.4 * 1.25) = 1.75
}
h1 {
font-size: 3em;
margin-bottom: 3.5rem; // Double the base value for a larger gap (1.75 * 2) = 3.5
}
h2 {
font-size: 2em;
margin-bottom: 1.75rem;
}
h3 {
font-size: 1.5em;
margin-bottom: 1.75rem;
}
.page-container {
padding: 3.5rem 2rem; // 3.5 is double the base value
}
/* Simple Sass Implementation */
$base-line-height: 1.4;
$base-font-size: 1.25rem;
$vertical-rhythm: $base-line-height * $base-font-size;
body {
line-height: $base-line-height;
}
p {
font-size: $base-font-size;
margin-bottom: $vertical-rhythm;
}
h1 {
font-size: 3em;
margin-bottom: $vertical-rhythm * 2;
}
h2 {
font-size: 2em;
margin-bottom: $vertical-rhythm;
}
h3 {
font-size: 1.5em;
margin-bottom: $vertical-rhythm;
}
.page-container {
padding: ($vertical-rhythm * 2) 2rem;
}
Note that rem
is used for spacing as it is not influenced by the font-size
of the element.
The bottom aligned baseline grid is a stricter implementation of vertical rhythm. In web, text is vertically aligned to the center of the line-height
. This can be troublesome for large text as there will be excessive space on the top and the bottom. In print, this issue is avoided as text is aligned to the bottom of the baseline grid.
It's also possible to fix the issue without a baseline grid by applying a negative margin-top
and a smaller margin-bottom
to large texts.
There is no easy way to apply a bottom aligned baseline grid that works for different typefaces, font-size
, and resolutions. It is highly recommended to use a typographic baseline library such as Sassline or MegaType.
Remember that vertical rhythm is just a guideline, and that the baseline grid is imaginary. It does not need to be pixel perfect for every element, nor does it need to be followed at every instance.
Color provides a huge visual distinction and is an important part of typography.
#000
as your body text color. Instead, use a very dark gray such as #333
.opacity
instead of a lighter color. Click here for an in-depth explanation.In a printed document, don’t underline. Ever. It’s ugly and it makes text harder to read. Practical Typography
By default, underlines don't look great in the web either. Fortunately, there is a method involving background-image
to style underlines to make them look appealing. Here is a Sass implementation of the original underline gist by Adam Schwartz:
@mixin text-underline-crop($background) {
text-shadow: .03em 0 $background,
-.03em 0 $background,
0 .03em $background,
0 -.03em $background,
.06em 0 $background,
-.06em 0 $background,
.09em 0 $background,
-.09em 0 $background,
.12em 0 $background,
-.12em 0 $background,
.15em 0 $background,
-.15em 0 $background;
}
@mixin text-background($color-bg, $color-text) {
background-image: linear-gradient($color-text, $color-text);
background-size: 1px 1px;
background-repeat: repeat-x;
background-position: 0% 95%;
}
@mixin text-selection($selection) {
&::selection {
@include text-underline-crop($selection);
background: $selection;
}
&::-moz-selection {
@include text-underline-crop($selection);
background: $selection;
}
}
@mixin link-underline($background, $text, $selection){
@include text-underline-crop($background);
@include text-background($background, $text);
@include text-selection($selection);
color: $text;
text-decoration: none;
*,
*:after,
&:after,
*:before,
&:before {
text-shadow: none;
}
&:visited {
color: $text;
}
}
/* Example usage */
a {
@include link-underline(#fff, #333, #0BF);
}
It is highly recommended to reserve underlines only for hyperlinks. This is a trend that the majority of websites follow, and deviating from it may cause confusion.
Congratulations for making it to the end of this handbook. Typography on the web — a medium where the user can be on any device in any resolution — is extremely difficult. When I first started designing websites years ago, I found it near impossible to find up-to-date information on best web typographic practices. There were many blog posts by experts with contradicting information, and existing books on the subject of web typography rarely go into details on technical implementation. Typography Handbook aims to solve this problem and provide almost everything that a beginner would need to know to create industry-standard typographic elements. I hope that this goal succeeded with you.