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
Formula
resolved value = reference value × percentage
Relative to
Defined by the property where % appears
Support
Universal

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 */
Demo — What % resolves to, per property
width: 50%
50%

50% of parent's inline-size

height: 50%
50%

50% of parent's block-size (needs explicit parent height)

padding: 10%
10%

10% of parent's inline-size — even padding-top!

translate(50%, 50%)
50%

50% of the element's own size

background-position: 75% 50%

75% of (container − image)

border-radius: 50%
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.

Use when
  • 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
Avoid when
  • When the reference isn't obvious to the reader — padding-top: 10% can surprise
  • height: 100% without a defined chain of parent heights
  • % on font-size across deeply nested components — prefer rem
  • 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

Sources