Web development for beginners: Styling HTML via CSS

[2025-10-10] dev, learning web dev, css
(Ad, please don’t block)

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.

HTML is for content, CSS is for presentation  

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.

What can we configure via CSS?  

These are some of the things that we can configure via CSS:

  • Backgrounds can be colored, show images, etc.
  • Fonts: We can use various fonts (even ones we download from somewhere) and change font sizes, font weights (bold etc.), font styles (italics etc.), font decorations (underline etc.), and more.
  • Spacing: margins, spacing between paragraphs, etc.
  • We can add borders around various elements.
  • Layout: There are many ways in which we can arrange the various elements of a web page – e.g., we can make it so that the actual content occupies most the page but leaves room for a disebar on the left.

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.

CSS syntax  

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:

  • A block in curly braces contains a sequence of declarations. In this case, there is only one declaration.
  • The selector 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 (;).

Kinds of rules  

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 property values  

CSS has many kinds of values. It’s important to distinguish:

  • Syntax: How is a value written?
  • Semantics: What is the value created by the syntax?

Human languages such as English make the same distinction:

  • Syntax: the two words “this tree”
  • Semantics: the actual tree

The syntax of CSS property values  

These are some common syntaxes for values:

  • An identifier (name) such as red consists of a sequence of characters (mostly letters and hyphens – more on that soon).
  • A number consists of a sequence of digits and can have a decimal point: 24, 1.2
    • A number can be followed by a unit such as px for pixels: 100px
    • A number can express percentages: 50%
  • A quoted string is a sequence of arbitrary characters in single quotes or double quotes:
    • 'single-quoted string'
    • "double-quoted string"

Roughly, an identifier consists of a sequence of characters:

  • The first character must either be a letter (a-z etc.) or an underscore (_).
  • The zero or more remaining characters can be letters (a-z etc.), digits (0-9), underscores (_) and hyphens (-).
  • The sequence can start with a single -, followed by first and remaining characters.
  • The sequence can start with a double --, 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

Types of CSS property values  

These are types of property values that we’ll explore later in this chapter:

  • Lengths are often numbers with units such as px (pixels)
  • Degrees are often numbers with the unit deg (degrees)
  • Colors are specified using a variety of syntaxes – e.g.:
    • Names: green
    • Mixed from red, green and blue – e.g., red: rgb(100% 0% 0%)

Where can we put CSS content?  

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.
  • The 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.

Material on CSS’s syntax  

Selectors  

Given a set of HTML elements, a selector picks none, some, or all of them.

Exercise: Try out selectors as you learn them  

Go to html/tools/css-selector-test.html and enter selectors at the bottom.

Simple selectors and compound selectors  

These are some common simple selectors (try out the examples in CSS Selector Test):

  • Type selector: tag selects all HTML elements whose name is tag
    • Example: p
    • Example: li
  • Class selector: .class selects all HTML elements that have a class whose name is class
    • Example: .first
  • ID selector: #id selects all HTML elements that have an ID whose name is id
    • Example: #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

Selector lists  

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 }

Combinators  

A combinator is a symbol such as > or a space that, when put between two compound selectors, produces a new selector.

The descendant combinator ( )  

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
  • The first one is a compound selector that selects all <li> that have the class first.
  • The second one is the descendant combinator applied to the simple selector li and the simple selector .first. It selects all elements that are anywhere inside an <li> and have the class first.

The child combinator (>)  

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).

The next-sibling combinator (+)  

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;
}

Material on selectors  

Determining CSS property values for an HTML element  

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:

    • An inherited property gets its default value from the property value of the parent element. That process is called inheritance.
    • Every other property has an initial value that is used as a default. The initial value is part of the standard definition of a non-inherited property.

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.

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)

  • Component A counts how many “ID-like” parts a selector has.
  • Component B counts how many “class-like” parts a selector has.
  • Component C counts how many “element-like” parts a selector has.

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:

  • Is one component higher than the other? Then that component’s selector has a higher specificity.
  • Otherwise, we proceed to the next component.
  • Etc.

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:

  • (1, 2, 4) > (1, 2, 3)
  • (0, 0, 99) < (1, 0, 0)
  • (0, 1, 2) > (0, 0, 2)
  • (1, 2, 4) = (1, 2, 4)

Exercise: Experiment with specificities  

Got to “Specificity Calculator” by Keegan Street and check which specificities it computes for various selectors.

The cascade: collecting relevant declarations  

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.

Material on the CSS cascade  

Default values of CSS properties: inherited values vs. initial values  

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

Example: the inherited 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:

  • Top level: The font size is relatively small.
  • .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:

  • Top level: The background is transparent (this initial value of 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.

Specifying lengths  

There are many ways in which lengths can be specified in CSS. Let’s take a look.

Absolute lengths  

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  

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:

  • If we specify the width of a <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.
  • If, on the other hand, we use the width 60ch (roughly: “60 characters”) then the <div> automatically grows as needed. ch is interpreted relatively to the size of the current font.

Font-relative lengths  

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.).

Percentages  

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”

Which length unit to use?  

  • 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.

Material on CSS length units  

Specifying angles in CSS  

CSS needs angles for several things. In this chapter, we’ll encounter two of them:

  • Some ways of specifying colors use angles for hues.
  • We can slant some fonts by a small positive or negative degree.

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.

Specifying colors in CSS  

There are many ways in which we can specify colors in CSS. In this section, we look at some common ones.

Background knowledge: color spaces – sRGB vs. Display P3  

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.

Color notation: named colors  

We have already encountered named colors such as red, gray, lightblue, chartreuse, etc. The standard for CSS color has an exhaustive list of them.

Color notations: RGB (Red Green Blue)  

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:

  • Using only red produces red: (255, 0, 0)
  • Mixing red and green produces yellow: (255, 255, 0)
  • Using no color (no light) produces black: (0, 0, 0)
  • Mixing red, green and blue produces white: (255, 255, 255)

These are common ways of writing RGB colors:

  • Three two-digit hexadecimal numbers (hexadecimal FF is decimal 255):
    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 percentages

RGB colors use the sRGB color space.

Color notation: HSL (Hue Saturation Lightness)  

HSL stands for Hue Saturation Lightness. HSL colors are specified via three components:

  • Hue: Which color pigment do we want to use? The available colors are arranged in a circle. We specify the pigment via an angle that is interpreted as a location on that circle. The circle starts like this:
    • Top (0°): red
    • Top right (39°): orange
    • Top right (60°): yellow
    • Bottom right (120°): green
  • Saturation: How much pigment do we want to use? 100% means “use as much pigment as possible”. Then the color is as vibrant as possible. 0% means “use no pigment”. Then all color is drained and the hue doesn’t matter. Lightness still matters and produces shades of gray, from black to white.
  • Lightness: How light do we want the pigment to be? Think of it as shining light on the pigment:
    • The extreme “no light” (0%) always produces black.
    • The extreme “all of the light” (100%) always produces white.

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.

Specifying shades of gray via HSL  

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 */

Color notation: OKLCh (Oklab lightness, chroma, hue)  

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:

  • Lightness is either a percentage or a number between 0.0 and 1.0.
  • Chroma is a value that starts at 0 and has no actual upper limit. However, in practice it does not exceed 0.5.
  • Hue is an angle.

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 vs. HSL  

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.

Which color notation to use in CSS?  

  • 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.

Exercise: Play with color notations  

Now it’s time for you to play with the color notations:

  • Go to the website “HSL Color Picker” by Brandon Mathis.
  • It has five color pickers (of which initially only HSL is shown) – try out the following ones: OKLCh, HSL, RGB.

Material on CSS colors  

The CSS box model  

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):

  • The actual content is at the center.
  • It is surrounded by space that is called padding.
  • That space is surrounded by a frame that is called a border.
  • The border is surrounded by space that is called margin.

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).

Exercise: Play with the box model  

Got to html/tools/box-model-test.html and edit the CSS to see how it affects the box model boxes.

CSS box sizing  

Box sizing determines what box model sizes such as the width refer to: the content box or to the border box?

  • The default value for the property box-sizing is content-box: The width of an HTML element is the width of the content box.
  • We can change that and set 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?

  • With content-box, if we start with a given width and increase the horizontal padding then the border box becomes wider.
  • With 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.

Example: 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:

  • The first inner box uses 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.
  • The second inner box also uses 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!
  • The third inner box switches to border-box sizing and still fits inside the outer box. Adding padding and a border has not increased its width as we perceive it.

Which kind of box sizing should we use when?  

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:

Providing a better default for box sizing  

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;

Styling borders  

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. */

Material on the CSS box model  

Viewport vs. web page  

The viewport is the window through which we look at a web page:

  • In desktop browsers, it changes with the size of the window.
  • In mobile browsers, it usually fills most of the screen.

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.

Background knowledge: typography  

Characters vs. glyphs  

When it comes to symbols such as letters and digits, there is an interesting subtle distinction:

  • A character is what we work with when we use computers.
  • A glyph is how a character is displayed on screen, printed on paper, etc.

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.

Typefaces are families of fonts  

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:

  • Helvetica Regular, Helvetica Oblique
  • Helvetica Light, Helvetica Light Oblique
  • Helvetica Bold, Helvetica Bold Oblique

This typeface has two style axes:

  • Axis weight: light, regular, bold
  • Axis slant: upright (implicit if there is no “oblique”), oblique

Typefaces: serif vs. sans-serif  

In typography, a serif is an extra marker (think small line) at the end of a stroke in a letter:

  • A serif typeface has serifs.
  • A sans-serif typeface does not have serifs (“sans” means “without” in French).

This is an example of a serif typeface and a sans-serif typeface:

Typefaces: proportional vs. monospaced  

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:

  • DejaVu Sans consists of fonts such as DejaVu Sans Bold.
  • DejaVu Sans Mono consists of fonts such as DejaVu Sans Mono Bold.

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:

  • Con: Monospaced typefaces are more difficult to read and typos are harder to spot.
  • Pro: It’s easy to line up text inside lines – e.g. to align content after colons, to format tabular data or to create “text art” diagrams. Note that indenting lines works well either way.
  • Pro: Many monospaced typefaces were created for code and have more more distinct letters – e.g., it’s easier to tell apart the digit “0” from the capital letter “O” and the capital letter “I” from the lowercase letter “l” (see DejaVu Sans and DejaVu Sans Mono above).
  • Pro: In documentation, code is also written in monospaced typefaces – which means it’s easy to distinguish from English.

Font styles: oblique vs. italic  

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):

Fonts: optical size  

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.

Font formats  

Traditional (static) fonts: one style per file  

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:

  • TrueType (.ttf) is the oldest font format in this list. It was created in the 1980s by Apple.
  • OpenType (.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.

Variable fonts: one or more style axes per file  

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:

  • Google Fonts has a list of variable fonts that you can download for free. More on how to use such fonts later in this chapter.

Exercise: Play with variable fonts  

Which font format to use?  

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

Material on font formats  

Working with fonts in CSS  

Registering custom fonts via @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.

CSS property 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:

  • Use DejaVu Sans if it is available.
  • Otherwise, use any kind of sans-serif font family.

CSS property 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 */

CSS property 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;

Downloading custom fonts  

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:

  • It introduces an additional point of failure: If Google’s servers go down, your website can’t use the fonts.
  • Google can track your users (but I don’t know to what degree they are actually doing that) and in some countries you may be legally required to tell your users about such potential tracking.

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:

  • Use the search function (text field at the top) to find a font that you like. Note the “Filters” button which has many interesting criteria – e.g.:
    • Technology [used by fonts]: Variable, Color, None
    • Serif vs. Sans Serif
    • Appearance: Monospaced, Stencil, Pixel, etc.
  • Go to the page of your font and click the “Get font” button so that it is added to your shopping bag.
  • Go to your shopping bag and click on “Get embed code”.
    • Go to “Web” > “@import” – which provides two important pieces of data:
      • “Embed code in the <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.
    • “CSS class(es)”: This is how you use the fonts for text.
  • Go back to the font’s homepage and click on “Download all” to download the font files to your drive.

Example setup: 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.

Material on handling fonts in CSS  

Styling text with CSS  

In this section we look at ways of styling text other than changing fonts.

CSS property 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:

CSS property font-size  

These are some of the values we can use for the property font-size:

  • Absolute size: xx-small, x-small, small, medium, large, x-large, xx-large, xxx-large
  • Relative size: larger, smaller
  • Length: 1.5em, 2.1rem, etc.
  • Percentage: 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.

CSS property line-height  

Factors to consider when choosing a 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.

    • As an aside, a line length of 50–75 characters is considered to be best for readability (source). We can either use 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?

  • Length: 1.5em, 2.1rem, etc.
  • Number: a factor that is multiplied with the font size
  • Percentage: also to be multiplied with the font size
  • 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.

CSS property 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):

Coloring text  

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:

Material on styling text via CSS  

CSS property 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>.

Nesting CSS rules  

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;
}

Why nest CSS rules?  

Nesting has two benefits:

  • Nested rules are often easier to read.
  • We need to type less code.

More selectors  

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.

Pseudo-classes  

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.

The structural pseudo-class :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.

The dynamic user action pseudo-class :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.

The target pseudo-class :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  

Pseudo-class functions attach additional data to a pseudo class. That data is called arguments or parameters

The pseudo-class function :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?

  • CSS goes through values of n: 0, 1, 2, 3, etc.
  • CSS converts each input integer 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)
    • The first three children are selected.
  • 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:

Exercise: Use :nth-child()  

The matches-any pseudo-class function :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  

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.

Exercises: Play with CSS selectors  

Material on advanced selectors  

CSS variables  

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:

  • It makes it clear that the custom properties are not directly style-related.
  • :root is more specific than html.

Why are CSS variables useful?  

Compared to using literal values directly, CSS variables have two benefits:

  • We can use descriptive names and see the purposes of values.
  • If we want to use a different value in every location of a variable, we only need to change one location: the definition of that variable.

CSS recipes  

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.

HTML: ensuring the page looks good in mobile phones  

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.

CSS variables  

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%);
}

A better default for box-sizing  

When we learned about box-sizing, we also learned how to set up a better default:

* {
  box-sizing: border-box;
}

Centering content  

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>:

    • There are no vertical (top, bottom) margins.
    • 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:

    • If the viewport is narrower than 10rem (plus padding), the browser shows horizontal scroll bars for the content.
    • If the viewport is between 10rem and 35rem wide (plus padding), any increase in width is added to the content.
    • If the viewport is wider than 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.

Styling headings  

There are several ways in which plain HTML headings can be improved.

Bottom borders for <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.

Creating heading links: manually vs. automatically (via JavaScript)  

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>

Highlighting the target of the current URL  

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.

Styling tables  

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:

  • By default, each table cell has its own border. We change that via border-collapse so that the borders of two adjacent cells are joined into a single border.
  • By default, the text inside <th> is centered. We change that via text-align – which affects the horizontal alignment of text.
  • Text in cells is centered vertically by default. We can see that in the first row of the table. We change that via vertical-align.
  • Lastly, we add a border and some padding.

With this CSS, the table now looks as follows:

Aligning table columns  

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:

  • It has an ancestor that has the class col1-right.
  • It is the first child of its parent (a <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">

How to explore the CSS standards.  

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.