Percentage · Chapter 3 of 7 · 1 unit
Percentage (%)
Fifty percent of what? — the whole subject, in one question.
% is not a single unit with a single rule. It’s a context-dependent value, and the meaning is set by the specific CSS property where it appears.
The same 50% can mean:
50% of the parent's width
50% of the parent's height
50% of the element's own size
50% of (container size − image size)
50% of the inherited font-size
50% of the box dimensions, per axis (for border-radius)
That’s why % is the most dangerous “simple” unit in CSS.
There is no single formula. The general shape is:
resolved value = reference value × percentage
But what counts as the reference value depends on the property:
width: 50%; /* containing block inline-size */
height: 50%; /* containing block block-size — if defined */
padding-top: 10%; /* containing block inline-size (yes — inline!) */
transform: translateY(50%); /* the element's own height */
font-size: 120%; /* inherited font-size */
% resolves to, per propertywidth: 50%50% of parent's inline-size
height: 50%50% of parent's block-size (needs explicit parent height)
padding: 10%10% of parent's inline-size — even padding-top!
translate(50%, 50%)50% of the element's own size
background-position: 75% 50%75% of (container − image)
border-radius: 50%50% of each axis → ellipse from a rectangle
% looks like one unit, but it resolves against a different reference in every property. The most counter-intuitive case is padding andmargin: they're always measured against the parent'sinline size — including padding-top and padding-bottom.
Here is the reference table worth keeping at hand:
| CSS | % is relative to |
|---|---|
width: 50% |
the containing block’s width |
inline-size: 50% |
the containing block’s inline size (its width only in horizontal writing) |
height: 50% |
the containing block’s height, if that height is definite |
block-size: 50% |
the containing block’s block size, if that size is definite |
padding: 10% (all sides) |
inline-size of the containing block |
padding-top: 10% |
inline-size of the containing block — not height |
margin: 10% (all sides) |
inline-size of the containing block |
margin-top: 10% |
inline-size of the containing block — not height |
transform: translateX(50%) |
the element’s own width |
transform: translateY(50%) |
the element’s own height |
background-position: 50% 50% |
(container size − image size) on each axis |
border-radius: 50% |
width for horizontal radii, height for vertical |
font-size: 120% |
inherited font-size (parent’s computed) |
line-height: 150% |
element’s own font-size, but inherits as a length |
top: 50% / left: 50% |
containing block size on the matching axis |
width: 50%
The most intuitive case:
.child {
width: 50%;
}
If the containing block is 800px wide, then width = 400px.
In logical-property terms it’s clearer to think:
width (in horizontal writing) = inline-size of the containing block
For writing-mode neutrality, use:
.child {
inline-size: 50%;
}
height: 50% and the famous height: 100% trap
height: 50% is measured against the containing block’s height, but only if that height is defined.
.parent {
/* no height set */
}
.child {
height: 100%;
}
In ordinary flow, this often doesn’t work the way authors expect. If the parent’s height depends on its content, then a percentage height on the child has no definite reference and resolves to auto.
Working example:
.parent { height: 400px; }
.child { height: 50%; } /* = 200px */
Broken example:
.parent { height: auto; }
.child { height: 100%; } /* often resolves to auto, not "fill" */
That’s the source of the eternal question:
Why doesn’t
height: 100%work?
Because 100% of auto doesn’t give a definite height.
For “fill the visible viewport” layouts, you usually want min-height: 100dvh, 100svh or 100vh (or flex/grid stretch), not a chain of height: 100% without explicit heights.
padding: 10% and margin: 10%
This is the most counter-intuitive part of %:
.box {
padding-top: 10%;
}
padding-top is not measured against the parent’s height. It’s measured against the parent’s inline-size (width in horizontal writing).Why width? Vertical padding resolving against a possibly-unknown height would be circular — content height depends on padding depends on height. Width was the one measure the engine already knew. The oddity is load-bearing.
In horizontal writing:
padding-top: 10% = 10% of the containing block's width
The same is true for padding-bottom, padding-left, padding-right, and for every side of margin.
This is the basis of the classic aspect-ratio hack:
.ratio {
height: 0;
padding-top: 56.25%; /* 56.25 = 9 / 16 × 100 */
}
The 56.25% is computed from the width, producing a 16:9 aspect ratio.
Today the modern alternative is:
.ratio {
aspect-ratio: 16 / 9;
}
But the hack worked specifically because vertical padding is measured against horizontal size.
transform: translate(50%)
Percentages inside transform: translate() are measured against the element’s own reference box — not its parent.
.box {
transform: translateX(50%);
}
If .box is 200px wide, translateX(50%) = 100px.
For vertical:
.box {
transform: translateY(50%);
}
If .box is 80px tall, translateY(50%) = 40px.
The classic absolute-centering pattern combines both reference systems at once:
.modal {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
Two different percentages, two different reference boxes:
left: 50% = 50% of the containing block's width
top: 50% = 50% of the containing block's height
translateX(-50%) = -50% of the modal's own width
translateY(-50%) = -50% of the modal's own height
The same number 50%, two different systems of measurement — percentage logic in a single snippet.
background-position: 50% 50%
background-position is another case where “percent of the container” is incomplete.
The correct model:
offset = (container size − image size) × percentage
On each axis:
x offset = (container width − image width) × x%
y offset = (container height − image height) × y%
So:
.hero {
background-position: 50% 50%;
}
means:
align the image's 50% point with the container's 50% point
— i.e., the center of the image meets the center of the container.
If the image is the same size as the container, the difference is zero, and percentages produce no visible movement.
border-radius: 50%
border-radius: 50% doesn’t mean “50% of one dimension.” It means 50% on each axis, independently:
.pill-or-ellipse {
width: 200px;
height: 100px;
border-radius: 50%;
}
Horizontal radii resolve against the width, vertical radii against the height. A rectangle becomes an ellipse. A square becomes a circle:
.circle {
width: 100px;
height: 100px;
border-radius: 50%;
}
border-radius: 50% → ellipse from a rectangle, circle from a square
font-size: 120%
Percentage font-size behaves like em:
.child {
font-size: 120%;
}
means:
child font-size = inherited font-size × 1.2
— the same as font-size: 1.2em.
That means percentage font-size carries the same cascade trap as em. Repeat it on nested elements and the size compounds:
.nested {
font-size: 120%;
}
1.2 × 1.2 × 1.2 ...
For global typography, rem is usually safer.
line-height: 150%
Percentage line-height is computed relative to the element’s own font-size, but it inherits as a length:
body {
font-size: 16px;
line-height: 150%;
}
Computed line-height becomes a length: 24px. Descendants may inherit 24px, not the multiplier 1.5, which is the same bug pattern as line-height: 1.5em.
For base typography, prefer the unitless form:
body {
line-height: 1.5;
}
And use lh when you need the resolved length in another property:
.icon {
block-size: 1lh;
}
When to use %
Use % when the size should depend on the property’s specific reference value.
- Flexible layout columns:
width: 50% - Responsive media:
inline-size: 100% - Absolute positioning relative to a containing block
- Transform-based centering:
translate(-50%, -50%) - Circles and ellipses:
border-radius: 50% - Background placement:
background-position: 50% 50% - Legacy aspect ratios via
padding-top - Font scaling relative to a parent component
- When the reference isn't obvious to the reader —
padding-top: 10%can surprise height: 100%without a defined chain of parent heights%onfont-sizeacross deeply nested components — preferrem- Whenever you'd have to mentally simulate "% of what?"
Compact reference
| Property | % resolves against |
|---|---|
width |
containing block width (its inline size only in horizontal writing) |
inline-size |
containing block inline size |
height / block-size |
the matching containing-block dimension, if definite |
padding-* / margin-* |
containing block inline-size — always |
transform: translate(X, Y) |
the element’s own width / height |
background-position: X% Y% |
(container − image), per axis |
border-radius |
border-box width / height, per axis |
font-size |
inherited font-size |
line-height |
own font-size — but inherits as a length |
top / left / right / bottom |
containing block, matching axis |