This blog post is part of the series “Web development for beginners” – which teaches people who have never programmed how to create web apps with JavaScript.
To download the projects, go to the GitHub repository learning-web-dev-code
and follow the instructions there.
I’m interested in feedback! If there is something you don’t understand, please write a comment at the end of this page.
In the previous chapter, we used HTML to create unformatted content. In this chapter, we use CSS to configure the style of that content: We can change the color of the background, use various fonts, add vertical spacing, etc.
In this chapter, we learn the basics of CSS. In the next chapter, we use CSS for layout – positioning HTML elements on a web page.
CSS is an acronym for Cascading Style Sheets:
A style sheet is a chunk of CSS code. It consists of a series of rules that specify the styles of HTML elements (color, font, etc.).
Style sheets are cascading in that several of them can influence the presentation of HTML. To do that, CSS has to collect and combine all rules that apply to any given HTML element. The Merriam-Webster dictionary defines a cascade as “something arranged or occurring in a series or in a succession of stages so that each stage derives from or acts upon the product of the preceding”. That hints at order playing a role in CSS. More on that later.
These are some of the things that we can configure via CSS:
We’ll learn about layout in the next chapter. In this chapter we look at the other topics mentioned in this list. CSS can do even more, but that is beyond the scope of this series. One example is animating user interface changes – e.g., a dialog pane disappearing “into” the button which opened it. That can help users make sense of such changes.
A CSS style sheet is a series of rules. This is an example of a rule:
p {
background-color: red;
}
This style rule means: “Make the backgrounds of all paragraphs (the p
at the beginning) red!” Its syntax consists of two parts:
p
determines which HTML elements the declarations are applied to.Each declaration specifies a value for a CSS property of the selected HTML elements. In this case, the name of the property is background-color
and the value is red
. Name and value are separated by a colon (:
). The declaration ends with a semicolon (;
).
There are actually two kinds of rules:
We have already seen a style rule. This is the most common kind of rule. It changes the styles of HTML elements.
p {
background-color: red;
}
Additionally, there are at-rules which provide instructions that are not directly related to styling. One example is the @import
at-rule which adds the rules of a CSS file to the current CSS content:
@import "more-rules.css";
In addition, CSS supports comments: Notes for humans that explain what the CSS does. They are ignored by browsers and have no other effect or purpose.
/* Red is our corporate color */
CSS has many kinds of values. It’s important to distinguish:
Human languages such as English make the same distinction:
These are some common syntaxes for values:
red
consists of a sequence of characters (mostly letters and hyphens – more on that soon).24
, 1.2
px
for pixels: 100px
50%
'single-quoted string'
"double-quoted string"
Roughly, an identifier consists of a sequence of characters:
a-z
etc.) or an underscore (_
).a-z
etc.), digits (0-9
), underscores (_
) and hyphens (-
).-
, followed by first and remaining characters.--
, followed by remaining characters.Note that property names are also identifiers. These are examples of identifiers:
-webkit-transition
--highlight-color
text-decoration-style
p3
The following token is a number and not an identifier – because it starts with a digit: 123
These are types of property values that we’ll explore later in this chapter:
px
(pixels)deg
(degrees)green
rgb(100% 0% 0%)
CSS refers to HTML content. Therefore, we have to find a way to associate it with HTML. That can be done in two ways.
First, we can embed CSS content inside HTML documents via the <style>
HTML element. That element is usually put at the end of the <head>
:
<head>
...
<style>
p {
background-color: red;
}
</style>
</head>
Second, we can put a <link>
to a .css
file inside the <head>
an HTML document – whose rules are then applied to the HTML:
<head>
...
<link href="simple.css" rel="stylesheet">
</head>
Third, we can use the HTML attribute style
to specify CSS declarations for a single HTML element:
<p style="color: red; font-weight: bold;">
Danger!
</p>
Additionally, we can import CSS files from CSS code:
@import "more-rules.css";
Which one should we use?
<link>
is often preferred in projects that have more than a few lines of CSS.<style>
is convenient for small experiments because we don’t have to switch between an HTML file and a CSS files during editing.style
HTML attribute is mostly used for small tweaks and fixes and mostly avoided if possible.@import
lets us break up large CSS files into smaller, more manageable files.Given a set of HTML elements, a selector picks none, some, or all of them.
Go to html/tools/css-selector-test.html
and enter selectors at the bottom.
These are some common simple selectors (try out the examples in CSS Selector Test):
tag
selects all HTML elements whose name is tag
p
li
.class
selects all HTML elements that have a class whose name is class
.first
#id
selects all HTML elements that have an ID whose name is id
#navigation
*
selects all HTML elements (within a given set)A compound selector is a sequence of one or more simple selectors that directly follow each other (there must not be any spaces between them) – e.g. the following compound selector selects all list items that have the class first
:
li.first
We can separate selectors with commas and they select all HTML elements that would be selected by any single one of them (the union of the sets that they select individually).
As an example, consider the following CSS:
h1, h2, h3 { font-family: sans-serif }
It is equivalent to this CSS:
h1 { font-family: sans-serif }
h2 { font-family: sans-serif }
h3 { font-family: sans-serif }
A combinator is a symbol such as >
or a space that, when put between two compound selectors, produces a new selector.
This is an example of the descendant combinator:
nav li
It means: Select all <li>
that are somewhere inside a <nav>
.
Note that the following two selectors are different:
li.first
li .first
<li>
that have the class first
.li
and the simple selector .first
. It selects all elements that are anywhere inside an <li>
and have the class first
.This is an example of the child combinator:
li > *
This selector selects all children of all <li>
elements (all elements that reside directly inside an <li>
element).
This is an example of the next-sibling combinator:
ul + hr {
margin-top: 5px;
}
It selects all <hr>
that come directly after a <ul>
. One use case for this combinator is adding space (only) between divs:
div + div {
margin-top: 5px;
}
For a given HTML element, CSS performs the following steps for each property in order to determine its value (source):
Declared values (0+): CSS first collects all declarations relevant for element and property. Their values are called declared values.
Cascaded value (0–1): Then CSS uses an algorithm called the cascade in order to determine which of the declared values “wins”. The result is the cascaded value. If a property has no declared values then it doesn’t have a cascaded value either.
Specified value (1): Next, CSS makes sure that the property has a value (the so-called specified value). It either uses the cascaded value or – if there is no cascaded value – a default value. There are two kinds of properties:
There are more steps, but we don’t need to know those right now. In this section, we take a closer look at the cascade and at inheritance. Before we get to the cascade, we need to learn about the specificity of selectors.
Sometimes, several selectors compete with each other:
p.first {
color: green;
}
p {
color: red;
}
How does CSS decide which selector is better in this case? It uses selector specificity. The specificity of a selector consists of three comma-separated numbers in parentheses. The numbers A, B and C are called the components of the selector:
(A, B, C)
There are more selector parts than IDs, classes and elements (hence “ID-like” and not “ID” etc.) but that is beyond the scope of this series. These are examples of specificities:
*
: (0, 0, 0)li
: (0, 0, 1)ul li
: (0, 0, 2)ul.resources.first
: (0, 2, 1)#navigation .first
: (1, 1, 0)To determine which of two specificities is higher, we compare component by component, from left to right:
If all of components are equal then the specificities are also equal. This way of comparing two specificities with components is similar to how dictionaries compare two words with letters. Examples of comparing specificities:
Got to “Specificity Calculator” by Keegan Street and check which specificities it computes for various selectors.
After CSS has collected all declared values for a given property, it needs to pick one of them. It does so by looking at the specificities of the associated selectors and picks the value with the most specific one. If more than one selector is most specific, then the value that is mentioned last, wins.
As an example, consider the following CSS:
p.first {
color: green;
}
p.blue {
color: blue;
}
p {
color: red;
font-weight: bold;
}
Example 1: What declarations are used for this HTML element?
<p>
Only the selector p
applies and these two declarations are used:
color: red;
font-weight: bold;
Example 2: What declarations are used for this HTML element?
<p class="first">
Two selectors apply: p.first
and p
:
color: green; /* p.first */
color: red; /* p */
font-weight: bold; /* p */
The first 2 declarations are in conflict: green
wins because its selector is more specific.
Example 3: What declarations are used for this HTML element?
<p class="first blue"></p>
Three selectors apply: p.first
, p.blue
and p
:
color: green; /* p.first */
color: blue; /* p.blue */
color: red; /* p */
font-weight: bold; /* p */
The first 3 declarations are in conflict and blue
wins: Both green
and blue
have the same highest specificity but blue
comes later.
If there is no declaration that provides an explicit value for a given property, a default value is used. There are two mechanisms for doing that:
A few CSS properties are inherited – they use the value of their parent (the immediately surrounding HTML element). Since the parent’s value may also be a default, we can see that the value of an inherited property is often propagated from an ancestor to its descendants. Two examples of inherited properties are:
font-size
color
All other properties get an initial value that is defined for each such property. Interestingly, background-color
is not inherited.
In the CSS standards, properties are often defined via property tables. Let’s look at two (abbreviated) examples. Click on the links to see what those tables look like in the standards.
The property table for font-size
:
Name: | font-size |
Value: | ` |
Initial: | medium |
Applies to: | all elements and text |
Inherited: | yes |
Percentages: | refer to parent element’s font size |
The property table for background-color
:
Name: | background-color |
Value: | <color> |
Initial: | transparent |
Applies to: | all elements |
Inherited: | no |
Percentages: | N/A |
font-size
vs. the non-inherited background-color
Let’s explore the difference between the inherited font-size
and the non-inherited background-color
. Consider the following HTML (from html/css-basics.html
):
Top level
<div class="outer">
Outer text
<div class="inner">
Inner text
</div>
</div>
It is styled via the following CSS (I have omitted widths, margins and padding):
.outer {
font-size: 32px;
background-color: gainsboro; /* a light gray */
}
.inner {
border: thin solid black;
}
And displayed in browsers like this:
font-size
is inherited:
.outer
: The font size is explicitly declared to be larger..inner
: There is no declared value, so a default is used: the font size of the parent.background-color
is not inherited:
background-color
)..outer
: The background-color
is explicitly declared to be gainsboro
..inner
: There is no declared value, so a default is used: the initial value transparent
.There are many ways in which lengths can be specified in CSS. Let’s take a look.
On one hand, we have absolute lengths – which always remain the same:
cm
: centimeters (1cm
is 1/10 of one meter)
mm
: millimeters (1mm
is 1/10 of one centimeter)in
: inches (1in
is 2.54 cm)
pc
: picas (1pc
is 1/6 of one inch)pt
: points (1pt
is 1/72 of one inch)px
: pixels. 1px
is 1/96 of one inch: That means it doesn’t have anything to do with the resolution of the computer screen.Among these units, only px
is used when displaying HTML on a computer screen. We have already encountered it in the following example:
font-size: 32px;
However, CSS can also be used for HTML that’s meant to be printed (paper books etc.). And there, cm
, mm
, in
, pc
and pt
can all be useful.
Relative lengths are interpreted relatively to another length. Why would we want to do that? Each browser has a default font size that can be changed by a user – e.g., to enlarge the text they see on the web. Relative lengths enable us to automatically adapt to such font size changes:
<div>
to be 100px
then it is always fixed and if the user increases the default font size, its text may not fit well inside it anymore.60ch
(roughly: “60 characters”) then the <div>
automatically grows as needed. ch
is interpreted relatively to the size of the current font.These are lengths that are interpreted relatively to the size of the current font:
em
: 1em
is equal to the font-size
of its element (which is often inherited). These are two examples (taken from the standard for CSS units):
Make the line height of h1
elements 1.2 times greater than the font size of h1
elements:
h1 { line-height: 1.2em }
Make the font size of h1
elements 1.2 times greater than the font size inherited by h1
elements:
h1 { font-size: 1.2em }
ch
: 1ch
is, roughly, the width or height (depending on the dimension of the length) of an average character at the current font size.
These are lengths that are interpreted relatively to the size of the font of the root element (which is always <html>
in HTML):
rem
: 1rem
is equal to 1em
at the root level.rch
: 1rch
is equal to 1ch
at the root level.Why are root-relative lengths interesting? If the user changes the default font size, that value exists at the root level and may change as we descend deeper into the tree of HTML elements. rem
and rch
enable us to use relative lengths that are the same at every level. That is useful for sizing user interface elements and more. We’ll soon discuss how to choose between root-relative lengths (rem
etc.) and relative lengths (em
etc.).
As CSS values, percentages are always relative to something. What that is depends on the property and is defined by the standard for a property. These are two examples:
Property margin
(CSS standard): “Percentages refer to logical width of containing block.” That’s usually the width of the content area of that block.
Property font-size
(CSS standard): “Percentages refer to parent element’s font size”
For most lengths in user interfaces (widths, gaps, etc.) and font sizes, rem
is usually a good choice. In many ways, px
is more intuitive but with that unit, the user interface does not adapt to the preferred font size.
em
is a good choice whenever you want a size to adapt to the font size of the current HTML element – e.g. if you want to specify the gap between two paragraphs, it should be relative to the current font size, not relative to the font size of the root element.
Percentages can be useful for layout – a topic we’ll explore in the next chapter.
<length>
type” of the W3C standard “CSS Values and Units Module Level 3”CSS needs angles for several things. In this chapter, we’ll encounter two of them:
CSS supports the following units for angles:
deg
: degrees (360 degrees are a full circle)grad
: gradians (400 gradians are a full circle)rad
: radians (2π radians are a full circle)turn
: turns (1 turn is a full circle)If we don’t state a unit for a degree, deg
is used.
There are many ways in which we can specify colors in CSS. In this section, we look at some common ones.
A color space is a system for specifying colors that exist in the real world – e.g. on computer screens. A color gamut is the set of colors that a given color space can represent.
These are two color spaces that are used by CSS:
sRGB stands for standard RGB (Red Green Blue) and has a color gamut that covers about 35% of the visible color spectrum. It has been used for computer screens for a long time.
Display P3 (short: P3) is a newer standard and has a color gamut that covers about 45% of the visible color spectrum (source). It has some more reds and many more greens than sRGB (see diagram in the Wikipedia entry on P3). Newer computer displays can represent more colors than supported by sRGB, which is why they need color spaces such as P3.
We have already encountered named colors such as red
, gray
, lightblue
, chartreuse
, etc. The standard for CSS color has an exhaustive list of them.
RGB stands for Red Green Blue. RGB colors are specified via three components – often numbers between 0 and 255 (8 bits). These indicate the intensity of the three colors red, green and blue that are mixed into a single one: 0 means a color isn’t used at all; 255 means that it is used to the maximum. The mixing is done additively – i.e., we are mixing lights not paints:
These are common ways of writing RGB colors:
color: #FF0000; /* red */
color: #FFFF00; /* yellow */
color: #000000; /* black */
color: #FFFFFF; /* white */
rgb()
with decimal numbers between 0 and 255 that are separated by spaces:color: rgb(255 0 0); /* red */
rgb()
with percentages that are separated by spaces:color: rgb(100% 0% 0%); /* red */
rgb()
with a mix of decimal numbers and percentagesRGB colors use the sRGB color space.
HSL stands for Hue Saturation Lightness. HSL colors are specified via three components:
This is how we specify HSL colors in CSS:
color: hsl(0deg 100% 50%); /* red */
The percentage symbols after the last two components can be omitted. We can also use other degree units such as rad
for the hue.
HSL colors use the sRGB color space and are therefore limited by it.
HSL is convenient for specifying shades of gray:
We set the first two components to zero. The second component (saturation) being zero means: “Use no color pigment”. Therefore, the first component (hue) doesn’t matter. We set it to zero because that is short and simple.
We specify the shade of gray via the third component (lightness) and do so in percent.
These are some examples:
color: hsl(0 0 0%); /* black */
color: hsl(0 0 50%); /* dark gray */
color: hsl(0 0 75%); /* light gray */
color: hsl(0 0 100%); /* white */
OKLCh stands for Oklab Lightness Chroma Hue. The components are very similar to those of HSL (chroma means saturation) but appear in a different order:
These are examples of OKLCh colors in CSS:
color: oklch(62.8% 0.258 29deg); /* red */
color: oklch(96.8% 0.211 110deg); /* yellow */
color: oklch(0% 0 0deg); /* black */
color: oklch(100% 0 0deg); /* white */
How are the components specified?
OKLCh has two advantages over HSL:
HSL saturation and lightness are not aligned with how humans perceive color: Two colors with different hues but the same saturation and lightness don’t always appear equally bright to us (see this blog post on OKLCh for an example).
HSL is limited by the sRGB color space, OKLCh can represent any color visible to the human eye. Therefore, it can be translated to sRGB, P3 and even color spaces that go beyond P3.
We need OKLCh if we want to display colors whose vibrancy goes beyond the sRGB color space. Tip by Brandon Mathis (click on “Questions?” to see it):
OKLCh can be tricky to navigate directly. Try picking a color in HSL or HWB first, then switch to OKLCh and crank up the chroma to push it into those vibrant P3 territories. It’s like having a turbo button for your colors!
In other words: Coming up with an OKLCh color is complicated; changing it is intuitive.
RGB colors are a common notation for colors in material on CSS. Especially the #
notation with six hexadecimal digits is convenient due to its terseness.
Now it’s time for you to play with the color notations:
The space occupied by an HTML element is laid out according to the CSS box model. It consists of the following areas – each of which may or may not be empty (and then won’t be displayed):
This is how to think about these areas: The border delimits the boundaries of an element. Padding adds space inside it and margin adds space outside it (between elements).
Got to html/tools/box-model-test.html
and edit the CSS to see how it affects the box model boxes.
Box sizing determines what box model sizes such as the width refer to: the content box or to the border box?
box-sizing
is content-box
: The width of an HTML element is the width of the content box.box-sizing
to border-box
: The width of an HTML element is now the width of the border box.What is the difference between these two box sizing modes?
content-box
, if we start with a given width and increase the horizontal padding then the border box becomes wider.border-box
, increasing padding means that the border box stays put and the content box shrinks.The latter is usually more intuitive because when we look at an HTML element, we’d consider its width to go from border to border.
content-box
vs. border-box
Let’s look at an example that demonstrates why border-box
is usually more intuitive. This is the HTML:
<div class="outer">
<div class="inner">
box-sizing: content-box
</div>
<div class="inner padded" style="box-sizing: content-box">
box-sizing: content-box
</div>
<div class="inner padded" style="box-sizing: border-box">
box-sizing: border-box
</div>
</div>
This is the CSS:
.outer {
display: flex;
gap: 10px;
flex-direction: column;
width: 200px;
border: thin dotted black;
}
.inner {
width: 100%;
background: hsl(0 0 85%);
}
.padded {
padding: 5px;
border: 5px solid black;
}
This is what the result looks like on screen:
content-box
sizing and has a width of 100% – which means that we want it to fill the complete width of its parent’s content.content-box
sizing. It gets padding and a border via .padded
. As a result, it becomes wider than the outer box. That’s not what we want!border-box
sizing and still fits inside the outer box. Adding padding and a border has not increased its width as we perceive it.box-sizing
only matters if we set a size property for an HTML element:
width
, height
min-width
, min-height
max-width
, max-height
Therefore, we don’t need to think about it for inline elements and block elements that are sized automatically. If we do set a size then:
border-box
tends to be more intuitive and works well if, e.g., we place sized elements in grids.content-box
can be useful for wrapper elements (elements that wrap other elements) – where we are interested in the width of the content, not in the width of the whole element.Related material:
Miriam Suzanne recommends adding the following CSS to your style sheets, in order to make border box sizing the default:
* {
box-sizing: border-box;
}
The argument against using more complicated CSS (as has become a common practice) and configuring box-sizing
to be inherited is as follows: content-box
sizing is useful for wrapper elements, but we don’t want all of their descendants to switch to it too. In other words: Changing one element to content-box
should not affect its descendants.
width
and height
There are two properties for setting the width and the height of an HTML element, via length values:
width
height
padding
and margin
The properties padding
and margin
are shorthands for other properties – e.g., the following CSS:
padding: 1rem 2rem 3rem 4rem;
Is equivalent to:
padding-top: 1rem;
padding-right: 2rem;
padding-bottom: 3rem;
padding-left: 4rem;
Note that the order matters. You can memorize it as trbl
(“trouble”) or as “clockwise starting at noon”. However, even with that mnemonic in mind, I still sometimes write a comment:
padding: 5rem 8rem 5rem 8rem; /* trbl */
We can also specify fewer than four values – e.g.:
/* Used for: top & right & bottom & left */
padding: 5rem;
/* Used for: (top & bottom) (right & left) */
padding: 5rem 8rem;
These are CSS properties for styling borders:
border-width
accepts lengths and the following values:border-width: thin;
border-width: medium;
border-width: thick;
border-style
can be: solid, double, dotted, dashed, groove, ridge, inset, outset
border-color
can be any color.Values of border-style
:
Each of the border-*
properties is a shorthand – e.g., border-width
is a shorthand for:
border-top-width
border-right-width
border-bottom-width
border-left-width
border
: shorthand for width, style and color Property border
is a shorthand for the following properties:
border-width
border-style
border-color
Because each of these properties has a different type of value, we can mention them in any order:
border: thin solid gray;
border: solid thin gray;
border: gray solid thin;
/* Etc. */
box-sizing
property” in W3C standard “CSS Box Sizing Module Level 3”The viewport is the window through which we look at a web page:
If a web page is too wide to fit into the viewport, the viewport usually gets horizontal scrollbars. If a web page is too long to to fit into the viewport, the viewport usually gets vertical scrollbars.
When it comes to symbols such as letters and digits, there is an interesting subtle distinction:
Sometimes a single character is displayed using multiple glyphs – e.g.: The character “é” (as in “café”) has the acute accent (´
). It is displayed by combining two glyphs: the glyph for “e” and the glyph for the accent.
Note that in practice, character also often means glyph.
A typeface is a design for displaying letters, numbers and other symbols on screen, on paper, etc. A particular style (such as bold) of a typeface is called a font. That’s why a typeface is also called a font family.
Note that colloquially, font can also mean typeface.
Let’s look at an example: Helvetica is a typeface. On macOS, it consists of the following fonts:
This typeface has two style axes:
In typography, a serif is an extra marker (think small line) at the end of a stroke in a letter:
This is an example of a serif typeface and a sans-serif typeface:
Most typefaces are proportional: In general, letters have different widths and each letter takes up as much width as it needs.
However, there are also monospaced typefaces where each letter has the same width. This kind of typeface was needed for typewriters.
Below you can see a proportional typeface and a monospaced typeface:
Note that these really are to different typefaces:
However, they are both part of the same font superfamily (a family of font families) called “DejaVu”.
Monospaced typefaces are the default for computer source code (HTML, CSS, JavaScript, etc.). That has pros and cons:
In CSS, “font style” refers to two ways of changing the shape of a font:
oblique
slants characters and is mainly used for sans-serif fonts.italic
also slants characters but additionally changes the shapes of some or all characters even further. It is mainly supported by serif fonts.You can see the difference here (note how each letter of “gaffe” changes with italic but not with oblique):
In professional printing, most fonts change their shapes slightly as their sizes grow. Quoting Elliot Jay Stocks (source):
Optical sizing refers to the practice of type foundries creating slightly different versions of a typeface intended to be used at different sizes. Generally speaking, small (body or caption) optical sizes tend to have less stroke contrast, larger x-heights, wider characters, and more open spacing. Their large (or display) counterparts have refined features and tighter spacing—characteristics that would hinder their readability at small sizes.
Traditionally, typefaces are stored in multiple files, with one font per file. These fonts are all vector fonts which means that they are not defined via bitmaps (think pixels) but via outlines which can be displayed at any size without a loss in quality.
Two font formats can be used with both the web and operating systems:
.ttf
) is the oldest font format in this list. It was created in the 1980s by Apple..otf, .otc, .ttf, .ttc
) is an evolution of TrueType created in 1996 by Microsoft and Adobe. It provides more features for describing typographic behavior. One key feature is support for variable fonts (which we’ll look at soon).The remaining two font formats can only be used with the web:
Web Open Font Format (.woff
) files are TrueType or OpenType fonts, with data compression applied and XML-based metadata added. Both help browsers handle downloaded fonts more efficiently: On one hand, compression reduces download times. On the other hand, the prefixed metadata means that some font-related decisions can be made earlier (because the metadata is downloaded first). Additionally, WOFF files only work on the web, which makes some font vendors more willing to license them for the web. WOFF 1.0 was created in 2012 by Mozilla, Type Supply, LettError and other organizations.
Web Open Font Format 2 (.woff2
) supports better compression than the previous version (thanks to the Brotli algorithm). It was created in 2024.
The font formats TrueType and OpenType support collections of traditional fonts: Files that contain multiple font styles. A variable font is also a collection of styles but goes one step further: It supports one or more style axis. Per axis, we can pick from several (often many) values. With collections of fonts, the creators dictate (e.g.) which font weights are available. With a font that is variable along the weight axis, a user can decide more freely what they want: The axis is like a slider that the user can move back and forth in order to determine how heavy the font is. Variable fonts with multiple axes increase this flexibility even further. Because variable fonts support axes via outline-related features and not via multiple files, they also save memory when working with many styles.
The standard for variable fonts defines five predefined registered axes – but font designers can also define their own custom axes. These are the registered axes (the properties are covered in more detail later):
Axis | CSS property | Example values |
---|---|---|
Weight | font-weight |
bold , 380 |
Width | font-stretch |
110% |
Italic | font-style |
italic |
Slant | font-style |
oblique 14deg |
Optical size | font-optical-sizing |
none , auto |
Related resource:
In principle, any of the four formats is fine for the web – often, you don’t even have a choice: You have to use whatever file you can download. Considerations:
Do you want to both install the fonts for your operating system and use them in web projects? Then the upsides of only needing a single set of files can outweigh the downsides. Servers usually compress data when sending it over the web, so download speeds should be better than file sizes imply.
Another consideration is licensing: WOFF and WOFF2 fonts are more likely to be licensed for use on the web.
Otherwise, choose WOFF2: It has the smallest file sizes and is optimized for the web. You can also try and convert the other file formats to WOFF2 (do a web search for tools that do that).
How much smaller are WOFF2 files? In a test, Shash got the following results:
File format | Size |
---|---|
.ttf |
225 KB |
.woff |
94 KB |
.woff2 |
83 KB |
@font-face
Custom fonts must be registered via CSS – e.g., there are four rules for the font family “DejaVu Sans” in html/fonts/fonts.css
. These are two of them:
@font-face {
font-family: "DejaVu Sans";
src: url("DejaVuSans.ttf") format("truetype");
}
/* ... */
@font-face {
font-family: "DejaVu Sans";
src: url("fonts/DejaVuSans-BoldOblique.ttf") format("truetype");
font-weight: bold;
font-style: oblique;
}
It tells CSS that the .ttf
file contains a font whose weight is bold and whose style is oblique.
font-family
After we have registered a font family, we can use it for text via property font-family
:
html {
font-family: "DejaVu Sans", sans-serif;
}
font-family
supports two kinds of font family names:
Family name: The name of a font family – which is either provided by the operating system or by @font-face
. It’s best to always put the name in quotes.
Generic family: CSS supports several keywords for specifying characteristics of fonts – e.g.:
serif
sans-serif
monospace
cursive
(handwritten)fantasy
(playful)math
(intended for use with mathematical expressions)Like we did in the previous example, we can use more than one name. CSS uses the first font family that is available. Therefore, the previous declaration means:
font-weight
font-weight
specifies how “heavy” (i.e., bold) a font is. These are some of the supported values:
/* Absolute keyword values */
font-weight: normal;
font-weight: bold;
/* Keyword values relative to parent */
font-weight: lighter;
font-weight: bolder;
/* Numeric values with range [1, 10000] */
font-weight: 400; /* normal */
font-weight: 700; /* bold */
font-style
: italic
and oblique
font-style
is for italic and slant. Slant is specified via an angle between (and including) -90deg
and 90deg
. These are examples:
font-style: normal;
font-style: italic;
font-style: oblique;
font-style: oblique 10deg;
Google Fonts is a large repository of free custom fonts and a great resource for experimenting with them. They provide clear instructions for using their fonts. However, if you follow them, the fonts are hosted by their servers and downloaded directly to a user’s web browser. That has two downsides:
There is an easy way around those downsides: Host the fonts yourself – which means downloading a few files, adding them to your website and adding a little CSS. To do that, follow these steps:
@import
” – which provides two important pieces of data:
<head>
of your html”:<style>
@import url('...');
</style>
Open a new browser tab and go to the URL mentioned above. The resource at that URL contains @font-face
definitions with remote URLs. This is how CSS becomes aware of the fonts. Replace those remote URLs with paths to your local font files.html/css-basics.html
To see a concrete setup, check out html/css-basics.html
. Its font-related CSS is in two files:
html/fonts/fonts.css
contains @font-face
rules.html/shared/global.css
contains a font-family
declaration.In this section we look at ways of styling text other than changing fonts.
text-align
The CSS property text-align
can have these values (and more):
left
right
center
justify
Each of the following four paragraphs is aligned differently:
font-size
These are some of the values we can use for the property font-size
:
xx-small, x-small, small, medium, large, x-large, xx-large, xxx-large
larger, smaller
1.5em, 2.1rem, etc.
110%
(relative to font size of parent)Intriguingly, there is no precise definition of what a font size actually is. Quoting the Google Fonts website:
But there’s no specific part of a font that equals the point size, nor any combination of parts that necessarily add up to the point size. [...] The font bounding box may approximately equal the point size, but there is no specific, required relationship. Thus, how large a given font is at a given point size varies, and is font-specific. If you set two different fonts at 16 point, very likely one will be larger than the other.
line-height
In CSS, we use line-height
to control line spacing – the amount of vertical space between lines of text. That can significantly affect how easy text is to read. The following factors influence which line height we should choose (source):
Font: Given that sizes are font-specific, we can’t derive a line height from a font size in a way that works for all fonts.
Font size: Small font sizes require larger relative line heights (think factor by which the font size is multiplied) than large font sizes.
Length of lines: The longer the lines are, the large their line heights should be – so that people don’t lose track when they read them.
ch
units or assume that one em
is roughly two characters wide.Let’s examine how the same relative line height looks different for small font sizes and large font sizes: With the small font, it looks like the lines are relatively close together, while with the large font, it looks like they are farther apart.
With line lengths, the perceived difference in line height is more subtle. If you try to read the text, you’ll notice that the tight vertical spacing makes the second text harder to read than the first text.
line-height
values How can we specify line heights?
1.5em, 2.1rem, etc.
normal
: This value tells browsers to use a “reasonable” value based on font and font size.If you want to experiment with line heights, a good starting point can be the factor recommended by the CSS standard for the keyword normal
(source): a value between 1.0 and 1.2.
text-decoration
Text decoration adds adornments to text without changing the shapes of the glyphs. It is controlled via the following CSS properties:
text-decoration-line
: none, underline, overline, line-through, blink
text-decoration-style
: solid, double, dotted, dashed, wavy
text-decoration-color
text-decoration-thickness
: auto
, 0.1em
, etc.text-decoration
is a shorthand for the previous properties:
text-decoration: underline;
text-decoration: overline red;
text-decoration: none;
Additionally, we can use property text-underline-offset
to control how far below the baseline an underline is drawn.
Let’s explore how some of these properties work (you may not see the underlines in Safari – live version):
We can use to CSS properties to change the color of text:
color
sets the color of the text in an HTML element.background-color
sets the background color of all of an HTML element (not just of its text).Given this HTML:
<p class="white-text">
White text on black background
</p>
The following CSS:
.white-text {
color: white;
background-color: black;
}
Produces this output:
display
Property display
controls how an HTML element is displayed:
block
displays it as a block.inline
displays it as an inline.none
hides it.<img>
is normally an inline element (because that lets us put small images inside lines of text). However, we can use display
to display it like a block in some cases.
Another use case for display
is to show and hide user interface elements:
.additional-content {
display: none;
}
body.show-additional-content .additional-content {
display: block;
}
By default, all additional content is hidden. We can show it simply by adding the class show-additional-content
to the <body>
.
We can nest CSS rules – e.g., this CSS rule:
div {
padding: 0.5rem;
.some-class {
color: yellow;
}
}
Is equivalent to:
div {
padding: 0.5rem;
}
div .some-class {
color: yellow;
}
In other words, by default, nesting uses the descendant combinator. If we want to combine the outer and the inner selector differently, we need to use the nesting selector &
– e.g., this CSS rule:
div {
padding: 0.5rem;
&.some-class {
color: yellow;
}
}
Is equivalent to:
div {
padding: 0.5rem;
}
div.some-class { /* no space after div! */
color: yellow;
}
Nesting has two benefits:
We have already seen a few simple CSS selectors. In this section, we look at a few more advanced ones that are also often useful.
Some selectors such as ID selectors and class selectors categorize HTML elements via explicit traits (IDs and classes). Pseudo-classes provide a flexible mechanism for categorizing via less explicit traits.
:root
The structural pseudo-class :root
matches the root element of the document tree. In HTML, that is always the <html>
element. However, the selector :root
is more specific than the selector html
. CSS can style data other than HTML. :root
enables us to always target the root element – regardless of the data. As we’ll see later, by convention, it’s also used to define CSS variables.
:hover
The pseudo-class :hover
selects the HTML elements over which the user currently hovers with their cursor. Due to nesting, it often applies to more than one element. Note that you can only hover with a pointer device (mouse, trackpad etc.) and not with touch.
Given the following HTML:
<div class="square">
Hover with your cursor!
</div>
This CSS makes the background of the <div>
red whenever the user hovers over it:
.square:hover {
background-color: red;
}
You can see his code in action in html/css-basics.html
– which additionally demonstrates how hovering works with nested elements.
:target
The pseudo-class :target
selects the HTML element that the URL fragment of the current location refers to. If the current location has no fragment then there is no such element. We’ll use this pseudo-class later, in a CSS recipe for styling headings.
Pseudo-class functions attach additional data to a pseudo class. That data is called arguments or parameters
:nth-child()
The pseudo-class function :nth-child()
matches an element if it is (e.g.) the first child of its parent. These are some of the arguments we can use:
:nth-child(odd)
selects all children at odd positions (1st, 3rd, 5th, etc.).:nth-child(even)
selects all children at even positions (2nd, 4th, 6th, etc.).:nth-child(3)
selects the third child.:nth-child(2n+1)
selects all children at odd positions.The last notation follows this pattern: An + B
. It lets us specify zero or more positions of children. How does it work?
n
: 0, 1, 2, 3, etc.n
to an output integer like this: It multiplies n
by A
and adds B
. The result is only used if it is greater than or equal to 1.Let’s look at examples:
2n+1: 1 (2×0 + 1), 3 (2×1 + 1), 5 (2×2 + 1), ...
-n+3: 3 (-0 + 3), 2 (-1 + 3), 1 (-2 + 3)
n: 1, 2, 3, ...
n+5: 5, 6, 7, ...
3 (0×n + 3): 3
Consider the following HTML:
<ul>
<li>Hydrogen</li>
<li>Helium</li>
<li>Lithium</li>
<li>Beryllium</li>
<li>Boron</li>
</ul>
It is accompanied by this CSS:
#nth-child + section {
li:nth-child(2n+1) {
border-bottom: solid thin black;
}
}
The result looks as follows in a web browser:
:nth-child()
html/tools/nth-child-test.html
and try out different arguments for :nth-child()
.:is()
The pseudo-class function :is()
matches an element if any of its arguments match that element. The following CSS rule:
:is(h1, h2, h3, h4, h5, h6).numbered { ... }
Is equivalent to:
h1.numbered,
h2.numbered,
h3.numbered,
h4.numbered,
h5.numbered,
h6.numbered { ... }
Pseudo-elements let us refer to parts of HTML elements that are not elements themselves: Humans can see those parts, but HTML is not aware of them. One example is the first line of a <p>
– as displayed on screen. The following rule uses the ::first-line
pseudo-element to style that line:
#first-line + section {
p::first-line {
font-weight: bold;
}
}
You can see that rule in action in html/css-basics.html
.
CSS variables let us introduce names for CSS values. We define them via custom properties – properties whose names start with two hyphens:
:root {
/* Define the CSS variable --highlight-color */
--highlight-color: yellow;
}
We can access variables like this:
span.highlight {
/* Use the CSS variable --highlight-color */
background-color: var(--highlight-color);
}
CSS variables are inherited: If a variable is defined for a given HTML element then all descendants of the element can access it, too. That’s why variables that must be accessible everywhere are defined for :root
. html
works, too but :root
has two small advantages:
:root
is more specific than html
.Compared to using literal values directly, CSS variables have two benefits:
In this section, we look at CSS recipes that improve several aspects of HTML content. Check out html/css-recipes.html
to see them in action.
This isn’t really a CSS recipe, but what it does feels like CSS functionality. If we omit style information, the head of css-recipes.html
looks like this:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>CSS recipes</title>
</head>
By default, mobile phones show a wide viewport and zoom out so that it fits the screen of the phone. When mobile phones with browsers were still new, that helped with displaying web sites that were designed for desktop computers.
However, things have changed and, thanks to the second <meta>
tag, mobile phones will use narrow viewports and not zoom out.
At the beginning of the CSS, we define several CSS variables that we’ll use later.
:root {
--highlight-color: yellow;
--dimmed-text-color: hsl(0 0 65%);
--table-border-color: hsl(0 0 65%);
}
When we learned about box-sizing, we also learned how to set up a better default:
* {
box-sizing: border-box;
}
The following CSS centers the content of css-recipes.html
:
body {
box-sizing: content-box;
margin: 0 auto; /* vertical horizontal */
padding: 0.5rem;
min-width: 10rem;
max-width: 35rem;
}
The <body>
contains the actual content (in its content box). The above declarations ensure that it is always horizontally centered and adapts automatically to the size of the viewport:
The margin
inserts space between <body>
and <html>
:
auto
tells the browser to automatically pick a left margin and a right margin. It makes sure that all available horizontal space is evenly distributed between the two. Therefore, the body is always centered.We use min-width
and max-width
instead of a fixed width
so that the layout automatically adapts to the size of the viewport:
10rem
(plus padding), the browser shows horizontal scroll bars for the content.10rem
and 35rem
wide (plus padding), any increase in width is added to the content.35rem
(plus padding), any increase in width is added to the margins.The fixed padding
ensures that no matter how narrow the viewport, there is always space between the content and the borders of the page. In other words: The content never touches the borders.
box-sizing
: In this case, <body>
is a wrapper element for the content. Therefore, we want min-width
and max-width
to affect the content box, not the border box.
To see how changing the viewport affects content and margins, open css-recipes.html
in a desktop web browser and resize the window.
By default, link text has a different color than normal text: The color changes depending on whether a link has already been visited by the browser or not. If we don’t like that, we can give links the same color as the surrounding text:
a {
color: inherit;
}
I also find that the default underline is too close to the text above it. We can change that:
a {
text-underline-offset: 0.1rem;
}
I personally don’t do that anymore but we can also only show the underline when someone hovers over the link:
a {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
Whatever style you choose, I find it important that links are easy to detect. If only the color is different then that may be too subtle for many people to see. You can go to css-recipes.html
to play with three different link styles.
There are several ways in which plain HTML headings can be improved.
<h2>
I like <h2>
having a bottom border (which stretches across the whole width). That makes it easier to see the structure of content:
h2 {
border-bottom: solid thin gray;
}
Wikipedia styles its headings this way, as does css-recipes.html
.
Many websites have headings that link to themselves – e.g.:
<h2 id="styling-links">
Styling links
<a href="#styling-links" class="heading-link" aria-hidden="true">#</a>
</h2>
There is a link at the end of the heading whose text is “#”. If we click it, the browser jumps to the heading. Why is that useful? It enables us to quickly link to the heading: We click on the heading link and then copy the browser’s current URL.
Screen reader applications read web pages out loud, which helps people with impaired vision. The HTML attribute aria-hidden
tells screen readers to ignore the link text.
The following CSS styles the heading links:
.heading-link {
text-decoration: none;
color: var(--dimmed-text-color);
font-size: 0.5em;
}
We don’t want them to look like links so we remove the underlines via text-decoration
. And we want them to be relatively inconspicuous – hence the dimmed color and the smaller font size.
One approach to creating heading links is to manually type in the HTML as shown above. This approach is used in html/css-recipes.html
.
Alternatively, we can use JavaScript to add the heading links. html/css-basics.html
does that, via the following element at the end of its body:
<script type="module" src="shared/add-heading-links.js"></script>
Then the HTML of the headings becomes simpler:
<h2 id="styling-links">Styling links</h2>
We have already discussed the pseudo-class :target
. It enables us to highlight the element that the current URL points to:
*:target {
background-color: var(--highlight-color);
}
This functionality interacts well with heading links: If a user clicks on a heading link, its heading is highlighted – which tells them that the self-linking worked.
Consider the following table:
<table>
<thead>
<tr>
<th>Acronym</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>WWW<br>W3</td>
<td>World Wide Web</td>
</tr>
<tr>
<td>HTML</td>
<td>Hypertext Markup Language</td>
</tr>
<tr>
<td>HTTP</td>
<td>Hypertext Transfer Protocol</td>
</tr>
</tbody>
</table>
Without any styling, it doesn’t look great:
The following CSS brings several improvements:
table.framed {
border-collapse: collapse;
td, th {
text-align: left;
vertical-align: text-top;
border: thin solid var(--border-line-color);
padding: 0.3rem 0.6rem; /* vertical horizontal */
}
}
This is what the declarations do:
border-collapse
so that the borders of two adjacent cells are joined into a single border.<th>
is centered. We change that via text-align
– which affects the horizontal alignment of text.vertical-align
.With this CSS, the table now looks as follows:
Consider the following table:
By default, the text in each table cell is left-aligned. In this case, we want the first column to be aligned right. Interestingly, that is relatively tricky: We could add a class
or a style
to each cell of that column. However, that involves more manual work than it should: We should be able to only tell CSS once how to align that column. We can do that via the following code:
.col1-right td:nth-child(1) {
text-align: right;
}
The selector of this rule matches a <td>
element if:
col1-right
.<tr>
element).If a table cell matches this selector then its text is aligned right. Therefore, we can right-align the first column of a table by adding class col1-right
to it:
<table class="col1-right">
The official “CSS Snapshot” is an excellent starting point for exploring the various CSS standards. Especially useful: Its indices for terms, selectors, at-rules, indices, and values.