CSS units explained
A CSS unit is the “measuring tape” you attach to a number so the browser knows how to interpret it. 10 alone is usually meaningless in CSS. But 10px, 10rem, 10%, or 10vw tells the browser what that 10 actually means.
You’ll use units everywhere: sizing, spacing, typography, layouts, transforms, animations, and more. The trick is picking the unit that matches your goal:
-
Want something that stays consistent regardless of font size? Use
px(carefully). -
Want spacing/type that scales with user settings? Use
rem. -
Want something sized relative to its container? Use
%. -
Want something sized relative to the viewport? Use
vw/vh(or their newer cousins).
The three big families of units
For beginners, this mental model prevents a lot of headaches:
-
Absolute (screen-ish):
pxis the one you’ll actually use. Others exist, but are rarely useful on screens. -
Relative to typography:
em,rem, and friends. -
Relative to layout context:
%, viewport units, and grid’sfr.
Let’s meet them properly, with playgrounds you can poke.
Absolute units: px
px is the most common “absolute” unit on the web. It’s not a physical pixel in the real-world ruler
sense,
but it behaves like a stable unit for UI work.
The other absolute units (cm, mm, in, pt, pc) exist
mostly for print-style thinking.
On screens, they often map to CSS pixels anyway, so they’re not great “real measurement” tools.
px feels stable
Use px when you want something that doesn’t scale with font size: borders, hairline details, icon
nudges, etc.
For typography and spacing, many teams prefer rem so the UI respects user font settings.
.demo-box {
width: 220px;
border-width: 4px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.demo-wrap {
padding: 18px;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.demo-box {
padding: 18px;
border-style: solid;
border-color: #111;
border-radius: 14px;
background: #f2f2f2;
}
.demo-box strong {
font-family: ui-monospace, SFMono-Regular, monospace;
}
I am sized in px. Drag the sliders and watch me change.
Relative units: em vs rem
These are the two you’ll use constantly for typography and spacing.
-
emis relative to the current element’s font-size. -
remis relative to the root element’s font-size (usuallyhtml).
That one-line difference explains why em can feel “wild” in nested components.
It’s not broken. It’s just doing math… enthusiastically.
em compounds in nested components
If a parent increases its font size, and a child uses em, the child scales again.
That can be great (components scale as a unit) or annoying (unexpected very large buttons).
.card {
font-size: 16px;
}
.card h3 {
font-size: 1.5em;
}
.card .button {
padding: 0.8em 1.2em;
}
.card {
font-size: 20px;
}
.card h3 {
font-size: 1.5em;
}
.card .button {
padding: 0.8em 1.2em;
}
.card {
font-size: 24px;
}
.card h3 {
font-size: 1.5em;
}
.card .button {
padding: 0.8em 1.2em;
}
*,
::before,
::after {
box-sizing: border-box;
}
.card {
width: min(520px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f8f8f8;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.card h3 {
margin: 0 0 10px;
}
.card p {
margin: 0 0 14px;
line-height: 1.5;
}
.button {
display: inline-block;
border: 2px solid #111;
border-radius: 999px;
background: white;
font-weight: 700;
}
em demo
The title uses 1.5em and the button padding uses em, so everything scales with the card’s font-size.
rem is predictable across components
rem ignores nesting and always references the root font size.
That makes it excellent for consistent spacing systems and typography scales.
:host {
font-size: 16px;
}
.card h3 {
font-size: 1.6rem;
}
.card .button {
padding: 0.8rem 1.2rem;
}
:host {
font-size: 18px;
}
.card h3 {
font-size: 1.6rem;
}
.card .button {
padding: 0.8rem 1.2rem;
}
:host {
font-size: 20px;
}
.card h3 {
font-size: 1.6rem;
}
.card .button {
padding: 0.8rem 1.2rem;
}
*,
::before,
::after {
box-sizing: border-box;
}
.card {
width: min(520px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f2f2f2;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.card h3 {
margin: 0 0 10px;
}
.card p {
margin: 0 0 14px;
line-height: 1.5;
}
.button {
display: inline-block;
border: 2px solid #111;
border-radius: 999px;
background: white;
font-weight: 700;
}
rem demo
The sizes use rem, so they scale when the root font-size changes, not when the card’s font-size changes.
Important note: Throughout this tutorial, we’ll use :host instead of
:root in the Code Playgrounds because they use a shadow root. Everywhere
you see :host, just remember it's the equivalent of :root in a regular project.
Beginner rule of thumb: em vs rem
- Use rem for global spacing and typography scales (predictable, accessible).
- Use em when you want a component to scale with its own text (buttons are a classic example).
Percent units: %
Percentages are powerful, but they’re also the #1 cause of “why is this not working?” moments.
The reason: % depends on the property.
-
For
width,%is usually relative to the containing block’s width. -
For
height,%only works if the parent has a defined height (otherwise it becomes “auto-ish”). -
For
transform: translateX(%), the percent is relative to the element’s own size.
Percent width is the friendly one
.bar {
width: 60%;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
width: min(440px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f8f8f8;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.track {
border: 2px dashed #111;
border-radius: 12px;
padding: 12px;
background: white;
}
.bar {
height: 48px;
border-radius: 10px;
background: #ddd;
border: 2px solid #111;
display: grid;
place-items: center;
font-weight: 700;
}
The bar’s width is a percentage of the container.
Percent height needs a parent height
If the parent’s height is not set, height: 50% has nothing solid to calculate against.
So either set a height (or use layout methods like flex/grid) instead of fighting it.
.frame {
height: 340px;
}
.panel {
height: 50%;
}
.frame {
height: auto;
}
.panel {
height: 50%;
}
*,
::before,
::after {
box-sizing: border-box;
}
.frame {
width: min(440px, 92vw);
margin: 18px auto;
border: 3px solid #111;
border-radius: 16px;
background: #f8f8f8;
padding: 16px;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.panel {
border: 2px solid #111;
border-radius: 12px;
background: white;
display: grid;
place-items: center;
font-weight: 800;
}
.note {
margin: 0 0 12px;
line-height: 1.5;
}
When the frame has a real height, height: 50% behaves as you’d expect. When the frame height is auto, the percent has no fixed reference.
Panel
Viewport units: vw, vh, and the newer ones
Viewport units are relative to the browser window (the viewport):
-
1vw= 1% of the viewport width -
1vh= 1% of the viewport height
These are amazing for full-screen sections, responsive type experiments, and fluid layouts. But mobile browsers can change the usable viewport height as the URL bar shows/hides.
vh can be tricky on mobile
To address mobile browser UI changes, modern CSS added:
-
svh: small viewport height (more “safe” when UI is visible) -
lvh: large viewport height (when UI is hidden) -
dvh: dynamic viewport height (changes as the UI changes)
Learn much more about vh, and the more modern viewports units in the CSS VH Unit Interactive Tutorial.
Character units: ch and ex
ch is roughly the width of the “0” character in the current font.
It’s perfect for controlling readable line lengths, because typography is often measured in characters per line.
A common readability target is around 45–75 characters per line, depending on the design. That’s not a law of physics, but it’s a great starting point.
.prose {
max-width: 60ch;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
width: min(900px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f8f8f8;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.prose {
border: 2px dashed #111;
border-radius: 14px;
padding: 16px;
background: white;
line-height: 1.6;
}
.prose p {
margin: 0;
}
This paragraph is constrained with ch. Slide the max-width to feel how line-length affects readability. Your eyes will thank you (quietly, because eyes are not great at talking).
CSS Grid’s fr unit
The fr unit is used in CSS Grid. It means “a fraction of the available space”.
It only works in grid track sizing, like grid-template-columns.
In plain language: after fixed-size things take their space, the leftovers are split by fr.
fr splits remaining space
.grid {
grid-template-columns: 1fr 1fr 1fr;
}
.grid {
grid-template-columns: 2fr 1fr 1fr;
}
.grid {
grid-template-columns: 160px 1fr 2fr;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
width: min(500px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f2f2f2;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.grid {
display: grid;
gap: 12px;
}
.tile {
border: 2px solid #111;
border-radius: 14px;
background: white;
padding: 16px;
font-weight: 800;
display: grid;
place-items: center;
min-height: 80px;
}
Click different snippets to change the column sizes.
ABC
fr gets even better with minmax()
A classic responsive grid pattern is:
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
It means: “make as many columns as fit; each column must be at least 220px, and can grow to fill the row.”
We’ll use it again in the min()/max()/clamp() section.
Units beyond length
Not every unit is about distance. CSS also measures time, angles, and resolution. You’ll see these a lot in animations, transitions, and gradients.
Time units: ms and s
Animations and transitions often use:
-
250ms(milliseconds) -
0.25s(seconds)
Same duration, different outfit. Pick the one you find easier to read.
Many people prefer ms for UI transitions because “200ms” is very literal.
.pill {
transition-duration: 300ms;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
width: min(500px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f8f8f8;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.pill {
width: 220px;
height: 64px;
border: 2px solid #111;
border-radius: 999px;
background: white;
display: grid;
place-items: center;
font-weight: 800;
transition-property: transform;
transition-timing-function: ease;
}
.wrap:hover .pill {
transform: translateX(220px);
}
Hover the preview. The slider changes the transition time.
Move
Learn more about animations in the CSS Animation Interactive Tutorial, and about transitions in the CSS Transition Interactive Tutorial.
Angle units: deg, turn, rad
You’ll see angle units in transforms and gradients:
-
deg(degrees):90deg,180deg,360deg -
turn(full rotations):0.25turnis 90 degrees -
rad(radians): more common in math-heavy contexts
.square {
rotate: 15deg;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
width: min(500px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f2f2f2;
font-family: ui-sans-serif, system-ui, sans-serif;
display: grid;
gap: 14px;
}
.stage {
height: 220px;
border: 2px dashed #111;
border-radius: 14px;
background: white;
display: grid;
place-items: center;
}
.square {
width: 120px;
aspect-ratio: 1 / 1;
border: 3px solid #111;
border-radius: 14px;
background: #ddd;
display: grid;
place-items: center;
font-weight: 900;
}
deg
Learn more about rotations in the CSS Rotate Interactive Tutorial.
Modern unit combos: calc(), clamp(), min(), max()
These functions let you mix units safely:
-
calc()lets you do math with units (likecalc(100% - 2rem)). -
min()andmax()choose a smallest or largest value. -
clamp(min, preferred, max)locks a value between boundaries.
This is where CSS starts feeling like it has superpowers. (It always did. It just didn’t tell you.)
calc(): mixing % and rem
A common pattern is “full width minus padding”:
width: calc(100% - 2rem);
.inner {
width: calc(100% - 3rem);
padding: 1.5rem;
}
*,
::before,
::after {
box-sizing: border-box;
}
.outer {
width: min(500px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f8f8f8;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.inner {
border: 2px solid #111;
border-radius: 14px;
background: white;
}
.inner p {
margin: 0;
line-height: 1.5;
}
.note {
margin: 0 0 12px;
}
The inner box uses calc() to combine % and rem.
Resize the preview and move the slider. This stays responsive without hard-coding a fixed width.
Learn more about calc() in the CSS Calc Interactive Tutorial.
clamp(): fluid typography
A very practical use of viewport units is fluid type:
- Minimum size (comfortable on small screens)
- Preferred fluid size (scales with viewport)
- Maximum size (doesn’t become billboard text)
h3 {
font-size: clamp(1.2rem, 2.8vw, 2rem);
}
h3 {
font-size: clamp(1.4rem, 3.4vw, 2.4rem);
}
h3 {
font-size: clamp(1rem, 2vw, 1.6rem);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
width: min(900px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f2f2f2;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.card {
border: 2px solid #111;
border-radius: 16px;
background: white;
padding: 18px;
}
h3 {
margin: 0 0 8px;
}
p {
margin: 0;
line-height: 1.6;
}
Fluid type with clamp()
Resize the preview width. The heading grows with the viewport but stays within sensible limits.
Learn more about clamp() in the CSS Clamp Interactive Tutorial.
min() and max(): responsive sizing without media queries
These are fantastic for layouts:
-
width: min(560px, 92vw);means “don’t exceed 560px, but shrink on small screens.” -
gap: max(12px, 2vw);means “never go below 12px, but grow a bit on wide screens.”
.card {
width: min(520px, 92vw);
}
.card {
width: min(680px, 92vw);
}
.card {
width: min(410px, 92vw);
}
*,
::before,
::after {
box-sizing: border-box;
}
.stage {
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f8f8f8;
width: min(900px, 92vw);
margin: 18px auto;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.card {
border: 2px solid #111;
border-radius: 16px;
background: white;
padding: 18px;
}
.card p {
margin: 0;
line-height: 1.6;
}
min() makes a “max-width that still shrinks” style layout. Change snippets and resize the preview.
Learn more about min() and max() in the CSS Min() Max() Clamp() Interactive Tutorial.
Practical recipes
Let’s turn unit knowledge into patterns you’ll actually reuse.
Recipe 1: a simple rem spacing system
A clean approach is defining spacing tokens in rem.
That way spacing scales with the user’s base font size.
:host {
--space-1: 0.5rem;
--space-2: 1rem;
--space-3: 1.5rem;
}
.stack {
gap: var(--space-2);
}
.card {
padding: var(--space-3);
}
*,
::before,
::after {
box-sizing: border-box;
}
:host {
font-size: 16px;
}
.wrap {
width: min(900px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f2f2f2;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.stack {
display: grid;
}
.card {
border: 2px solid #111;
border-radius: 16px;
background: white;
}
.card h4 {
margin: 0 0 8px;
}
.card p {
margin: 0;
line-height: 1.6;
}
Card one
Spacing here uses rem tokens.
Card two
Easy to keep consistent across a site.
Recipe 2: responsive grid with fr and minmax()
This pattern is a go-to for responsive cards:
repeat(auto-fit, minmax(220px, 1fr))
.grid {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
width: min(500px, 92vw);
margin: 18px auto;
padding: 18px;
border: 3px solid #111;
border-radius: 16px;
background: #f8f8f8;
font-family: ui-sans-serif, system-ui, sans-serif;
}
.grid {
display: grid;
gap: 12px;
}
.card {
border: 2px solid #111;
border-radius: 16px;
background: white;
padding: 14px;
display: grid;
gap: 10px;
}
.card img {
width: 100%;
height: 140px;
object-fit: cover;
display: block;
border-radius: 12px;
border: 2px solid #111;
}
.card p {
margin: 0;
line-height: 1.5;
}
Slide the minimum width. The grid automatically changes how many columns fit.
![]()
Card A
![]()
Card B
![]()
Card C
![]()
Card D
Learn more about minmax() in the CSS Minmax Interactive Tutorial.
CSS units cheat sheet
Length units you’ll use most
- px: stable UI measurements (borders, fine-tuning)
- rem: scalable, consistent sizing based on the root font size
- em: scales with the current element’s font size (great for component scaling)
- %: relative sizing (but depends heavily on the property)
- vw/vh: viewport-based sizing
- dvh/svh/lvh: viewport height units that behave better on mobile
- ch: character-based widths (fantastic for readable text)
Layout-specific units
- fr: grid fraction unit (only in grid track sizing)
Non-length units
- ms / s: time (transitions/animations)
- deg / turn / rad: angles (transforms/gradients)
Common mistakes and debugging tips
Mistake: “Why doesn’t height: 100% work?”
Because percent heights need a parent with an explicit height.
If the parent’s height is auto, the browser can’t compute the percentage the way you expect.
- Fix: set a parent height, or use flex/grid to size things instead.
-
Alternative: use
min-height: 100dvhfor full-screen sections.
Mistake: “My em sizes exploded!”
That’s the compounding effect. Nested elements multiply the scaling.
If you want consistency across nesting, prefer rem.
Mistake: “100vh is weird on mobile”
Mobile browser UI changes the usable height as you scroll.
Try 100dvh (dynamic) or 100svh (safer smaller viewport).
Debugging tip: check computed values
In your browser DevTools, inspect an element and look at the Computed tab. You’ll see the final pixel value after all unit math is applied. This is the fastest way to answer: “relative to what?”
Closing thoughts
CSS units aren’t hard because the units are complex. They’re hard because they’re contextual. Once you learn what each unit is relative to, you can pick units intentionally:
- rem for scalable, consistent UI systems
- em for components that scale with their own text
- % for container-relative sizing (with awareness of the property)
- vw/vh/dvh for viewport-driven layouts
- fr for grid layouts that feel effortless
And when you want to mix the worlds together, reach for calc() and clamp(). They’re the duct tape of modern CSS, except… like, the good kind of duct tape.
