Skip to content
academia.sh

Lesson 25 / 26

CSS Units

Absolute, font-relative, and viewport-relative units; the em chain's calculation, rem's stability, and what a percentage resolves against.

Contents

Throughout this topic, lengths got written as pixels and rem, and the ch unit got left undefined. A length can get written in several separate forms, and the choice is not just a number format: some units track user settings, some do not.

This lesson separates units into three families and shows the conversions between them with calculation.

Three Families

Absolute unitspx, cm, mm, in, pt, pc, q. The ratios between them are fixed, and 1in = 96px gets taken by definition. px does not correspond to a device pixel; it is a reference unit and stays close to the same physical size regardless of the device pixel ratio.

Font-relative unitsem, rem, ch, ex, cap, lh, rlh. Their value depends on the font metrics in the context they get calculated in.

Viewport-relative unitsvw, vh, vmin, vmax, and their dynamic forms. They are defined as one percent of the viewport.

// units.mjs — the em chain, rem stability, and percentage references
console.log("--- em chain: multiplied again at every generation ---");
const chain = [
  { name: "html",    fontSize: "16px" },
  { name: "body",    fontSize: "1em"  },
  { name: "section", fontSize: "1.25em" },
  { name: "div",     fontSize: "1.25em" },
  { name: "p",       fontSize: "1.25em" },
  { name: "span",    fontSize: "1.25em" },
];
let parentPx = 16;
for (const node of chain) {
  let px;
  if (node.fontSize.endsWith("px")) px = parseFloat(node.fontSize);
  else px = parentPx * parseFloat(node.fontSize);
  console.log(`${node.name.padEnd(8)} font-size: ${node.fontSize.padEnd(7)} -> ${px.toFixed(3)}px  (parent ${parentPx.toFixed(3)}px)`);
  parentPx = px;
}

console.log("\n--- the same chain with rem ---");
const ROOT = 16;
parentPx = 16;
for (const node of chain) {
  const px = node.fontSize.endsWith("px") ? parseFloat(node.fontSize) : ROOT * parseFloat(node.fontSize);
  console.log(`${node.name.padEnd(8)} font-size: ${node.fontSize.replace("em","rem").padEnd(8)} -> ${px.toFixed(3)}px`);
}

console.log("\n--- em on the same element: font-size uses the outer value, padding the inner ---");
const parentFont = 16, ownFont = 1.5 * parentFont;
console.log(`parent font-size = ${parentFont}px`);
console.log(`font-size: 1.5em -> ${ownFont}px   (via the parent's value)`);
console.log(`padding:   1.5em -> ${1.5 * ownFont}px   (via its own font-size)`);
console.log(`line-height: 1.5em -> ${1.5 * ownFont}px`);

console.log("\n--- what a percentage resolves against ---");
const CONTAINING = { width: 640, height: 400, fontSize: 16 };
const REFERENCE = {
  "width":       ["containing width", CONTAINING.width],
  "padding-top": ["containing WIDTH", CONTAINING.width],
  "margin-left": ["containing width", CONTAINING.width],
  "height":      ["containing height", CONTAINING.height],
  "font-size":   ["parent font-size", CONTAINING.fontSize],
  "line-height": ["own font-size", 16],
};
for (const [prop, [description, base]] of Object.entries(REFERENCE)) {
  console.log(`${prop.padEnd(12)} %50 -> ${(base * 0.5).toFixed(1)}px   (reference: ${description} = ${base})`);
}

console.log("\n--- absolute unit conversions (1in = 96px) ---");
const PX = { px: 1, in: 96, cm: 96/2.54, mm: 96/25.4, q: 96/101.6, pt: 96/72, pc: 16 };
for (const [unit, value] of Object.entries(PX)) console.log(`1${unit.padEnd(3)} = ${value.toFixed(4)}px`);

console.log("\n--- ch and ex (in an example font) ---");
const size = 16, chRatio = 0.5, exRatio = 0.52;
console.log(`font-size=${size}px, 1ch=${size*chRatio}px, 1ex=${(size*exRatio).toFixed(2)}px`);
console.log(`max-width: 70ch -> ${70 * size * chRatio}px`);
--- em chain: multiplied again at every generation ---
html     font-size: 16px    -> 16.000px  (parent 16.000px)
body     font-size: 1em     -> 16.000px  (parent 16.000px)
section  font-size: 1.25em  -> 20.000px  (parent 16.000px)
div      font-size: 1.25em  -> 25.000px  (parent 20.000px)
p        font-size: 1.25em  -> 31.250px  (parent 25.000px)
span     font-size: 1.25em  -> 39.063px  (parent 31.250px)

--- the same chain with rem ---
html     font-size: 16px     -> 16.000px
body     font-size: 1rem     -> 16.000px
section  font-size: 1.25rem  -> 20.000px
div      font-size: 1.25rem  -> 20.000px
p        font-size: 1.25rem  -> 20.000px
span     font-size: 1.25rem  -> 20.000px

--- em on the same element: font-size uses the outer value, padding the inner ---
parent font-size = 16px
font-size: 1.5em -> 24px   (via the parent's value)
padding:   1.5em -> 36px   (via its own font-size)
line-height: 1.5em -> 36px

--- what a percentage resolves against ---
width        %50 -> 320.0px   (reference: containing width = 640)
padding-top  %50 -> 320.0px   (reference: containing WIDTH = 640)
margin-left  %50 -> 320.0px   (reference: containing width = 640)
height       %50 -> 200.0px   (reference: containing height = 400)
font-size    %50 -> 8.0px   (reference: parent font-size = 16)
line-height  %50 -> 8.0px   (reference: own font-size = 16)

--- absolute unit conversions (1in = 96px) ---
1px  = 1.0000px
1in  = 96.0000px
1cm  = 37.7953px
1mm  = 3.7795px
1q   = 0.9449px
1pt  = 1.3333px
1pc  = 16.0000px

--- ch and ex (in an example font) ---
font-size=16px, 1ch=8px, 1ex=8.32px
max-width: 70ch -> 560px

The em Chain

The first two blocks show the difference at the center of this lesson. On the same tree, a font-size: 1.25 declaration got written at every generation. Written with em, the value got multiplied on top of the previous one at every generation, and passed 39 pixels by the fifth generation. Written with rem, every generation used the root element’s size, and the value stayed fixed at 20 pixels.

em — depends on the element’s own computed font-size value. When used in the font-size property itself, the parent’s value gets used, because the element’s own value has not been computed yet. The chained growth comes from this.

rem — always depends on the root element’s font-size value. There is no chain.

The third block shows the distinction within the same element. font-size: 1.5em gave 24 pixels over the parent’s 16 pixels. On the same element, padding: 1.5em gave 36 pixels over the element’s own 24 pixels. The two declarations carry the same number but resolve against different bases.

This behavior of the em unit is not a flaw, it is the tool for component scaling: when a button’s padding gets written in em, the padding scales along with it once the button’s font size changes. Written in rem, it would stay fixed.

A Percentage’s Reference Varies by Property

The fourth block breaks a common assumption. A percentage does not always resolve against the same dimension.

width, margin, and — note — every padding value, including the vertical padding, resolves against the containing block’s inline dimension. The writing padding-top: 50% gives half the containing block’s width, not its height.

This rule, which looks strange, is the basis for a pattern: giving a box an aspect ratio. Because padding-top depends on width, a box whose height is proportional to its width can get built this way. The aspect-ratio property does the same job directly, and is the preferred way today.

height resolves against the containing block’s height, font-size against the parent’s font size, line-height against the element’s own font size.

The Cost of Writing Pixels

The fifth block gives the absolute units’ fixed ratios. These ratios are calculable, but the real dimension of the decision lies elsewhere.

A user can change the default font size in their browser. This setting affects the root element’s font-size value. The consequence is this:

  • Dimensions written with rem and em grow together with this setting.
  • Dimensions written with px do not change.

A page with font size written in pixels ignores the user’s reading setting. Zooming still works, but zooming enlarges the whole page — what the user wants is usually just the text getting bigger.

The applicable rule: font size, spacing, and box dimensions get written in rem or em. px gets used only for dimensions that genuinely need to stay fixed: thin borders, one-pixel dividers, shadow offsets.

Writing a fixed pixel value into the root element’s font-size brings the same problem back; this value either does not get written at all, or gets written with a percentage.

Other Font-Dependent Units

The last block defines the ch unit: the width of the “0” character in a font. The max-width: 70ch declaration written in the Box Sizing lesson finds its counterpart here — 560 pixels at the example’s ratio.

ex corresponds to lowercase letter height, cap to uppercase letter height, lh to the element’s line height. Their value depends on the chosen font; the 0.5 and 0.52 ratios in the example are sample values.

The lh unit is useful for building a spacing scale: writing margin-block-end: 1lh makes the gap exactly one line height, and the text’s baseline rhythm gets preserved.

Viewport Units

1vw is one percent of the viewport’s width, 1vh one percent of its height. vmin and vmax use the smaller and the larger of the two.

There is a distinction in the height units. In some environments, the browser interface hides and shows on scroll, and the viewport’s height changes. Three families get defined for this: svh is the smallest, lvh the largest, dvh uses the current height. Writing 100vh can produce a box that overflows while the interface is visible; 100svh prevents this.

Writing font size directly in vw is problematic: when the user zooms in, the viewport width gets measured with the zoom applied, so the text does not grow. For scaling typography, the viewport unit does not get used alone, but together with a lower bound — the tool for this gets taken up in the next lesson.

/* station.css — step 24: unit choice */
:root { font-size: 100%; }          /* preserve the user's setting */

main { max-width: 70ch; padding-inline: 1rem; }

h1 { font-size: 2.4414rem; margin-block-end: 0.5em; }

.status-badge { padding-block: 0.25em; padding-inline: 0.75em; }

.measurement-table th,
.measurement-table td {
  padding-block: 0.5rem;
  padding-inline: 0.75rem;
  border-block-end: 1px solid var(--line);
}

.card { box-shadow: 0 1px 2px rgb(28 39 51 / 0.06); }

The badge’s padding got written in em: when the badge gets used at a small font size, the padding shrinks too. The cell padding got written in rem: the table’s rhythm has to stay independent of the cell content’s size.

The heading’s bottom margin got written in em and got tied to the heading’s own size; large headings get large spacing.

The border and shadow offset stayed in px: these are dimensions that blur when scaled and are meant to stay fixed.

Summary

  • em depends on the element’s own computed font size; when used in the font-size property, it resolves against the parent’s value and grows in a chain by getting multiplied again at every generation.
  • rem always depends on the root element’s font size; there is no chain, and the value is unaffected by the tree’s depth.
  • A percentage’s reference varies by property: width and every padding value resolve against the containing block’s inline dimension, height against the containing block’s block dimension, line-height against the element’s own font size.
  • Dimensions written in px do not grow with the user’s font-size setting; font, spacing, and box dimensions get written in rem or em, px gets reserved for fine details that need to stay fixed.
  • Viewport height units have three families; svh is the smallest, lvh the largest, dvh uses the current height.

Next Step

Units are defined, but individual fixed values still get written one by one. A dimension may need to stay between two values, the smaller of two dimensions may need to get chosen, or one value may need to get subtracted from another. The next lesson — the course’s last lesson — defines the functions that do these calculations and wraps up the style sheet the course built.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close