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 units — px, 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 units — em, rem, ch, ex, cap, lh, rlh.
Their value depends on the font metrics in the context they get
calculated in.
Viewport-relative units — vw, 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
remandemgrow together with this setting. - Dimensions written with
pxdo 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
emdepends on the element’s own computed font size; when used in thefont-sizeproperty, it resolves against the parent’s value and grows in a chain by getting multiplied again at every generation.remalways 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:
widthand everypaddingvalue resolve against the containing block’s inline dimension,heightagainst the containing block’s block dimension,line-heightagainst the element’s own font size. - Dimensions written in
pxdo not grow with the user’s font-size setting; font, spacing, and box dimensions get written inremorem,pxgets reserved for fine details that need to stay fixed. - Viewport height units have three families;
svhis the smallest,lvhthe largest,dvhuses 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.