Understanding CSS Percentage
Tags: css, webdev
Published: 2021-09-08 08:28:31

Have you ever failed to understand how the percentages work in CSS? Ever wondered why it's so messed up and has zero logic sometimes? Well, I do. That's why I'm writing this post to share my understanding through my researches and readings with you.

## Percentage of what?

As a percentage, obviously there should be a target taken as a reference source. Most answers to this are the **parent block** of the element we assign the percentage. This is correct, but does not entirely cover all the cases. The most correct answer should be the **containing block**, meaning the block that contains our element and it doesn't have to be the direct parent.

Let's take a look at the below example:
{% codepen https://codepen.io/khangnd/pen/powbjEL %}

In this example, I created 3 nested `div`s, which are 3 squares with the following characteristics:

* The outmost **grandparent** div has a lightgray color with a size of **4x4**
* The **parent** div has a darker gray with a size of **2x2**
* And the red **child** div which I assigned the size of **50%**

If the percentage unit took the **parent** as the source, the size of the **child** should have been 1/2 of it, but no, the **child** actually has the size equals to the **parent** and 1/2 of the **grandparent** as you can see. The reason is the **grandparent** div is the true **containing block** of the **child** div, due to the fact that the **child** has `position: absolute`, corresponding to `position: relative` I have set in the **grandparent**.

Therefore, to identify which is the actual **containing block** of an element, it's entirely based on the `position` property of the element itself. *You can read more on [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block).*

However, for certain properties, the reference source for the percentage unit is neither the *parent* nor the *containing block*, instead, it is itself - **self element**.

## Percentage by property

### `width`/`height`

Pretty straightforward, as you have seen in the example above, when an element is assigned a percentage value to its `width`, the `width` of the **containing block** is taken as the reference source. Likewise, `height` of the element refers to the `height` of the **containing block**.

### `padding`

For `padding`, either vertical (`padding-top`/`padding-bottom`) or horizontal (`padding-left`/`padding-right`) refers to the `width` of the **containing block**.

Example:
{% codepen https://codepen.io/khangnd/pen/YzQWWRG %}

In this example,

* The **parent** div has a size of **6x4**.
* The **child** div has a size of **0**, but with `padding-top` and `padding-left` given **50%**

The result is that the **child** has a size equivalent to 1/2 `width` of the **parent**, which is a **3x3** square.

### `margin`

Similar to `padding`, the percentage of `margin` (both vertical and horizontal) refers to the `width` of the **containing block**.

Example:
{% codepen https://codepen.io/khangnd/pen/MWoejWd %}

In this example,

* The **parent** div has a size of **6x4**.
* The **child** div with `margin-top` and `margin-left` given **50%**

The result is that the **child** is positioned **3** units away from the top and left margins of the **parent** (1/2 `width` of the **parent**).

### `top`/`bottom`/`left`/`right`

For these properties (usually come with `position`), the vertical ones (`top`/`bottom`) refer to the `height` and the horizontal ones (`left`/`right`) refer to the `width` of the **containing block**.

Example:
{% codepen https://codepen.io/khangnd/pen/MWoerzw %}

In this example,

* The **parent** div has a size of **6x4**.
* The **child** div has `position: absolute` with `top` and `left` given **50%**

The result is that the **child** div is positioned **2** units away from the **parent**'s top edge (1/2 `height` of the **parent**), and positioned **3** units away from the **parent**'s left edge (1/2 `width` of the **parent**).

### `transform: translate()`

An incredible property for animation/transition, it also supports percentage value. However, this one does not refer to its **containing block**, but instead refers to **itself**.

Example:
{% codepen https://codepen.io/khangnd/pen/QWgEQvy %}

In this example,

* The **parent** div has a size of **6x4**.
* The **child** div has a size of **2x1** with `transform: translate(50%, 50%)`

The result is that the **child** div is positioned **0.5** unit away from the **parent**'s top edge (1/2 `height` of **itself**), and positioned **1** unit away from the **parent**'s left edge (1/2 `width` of **itself**).

### `background-size`

The `background-size` property brings the complexity of percentage unit to a new level 😄
The percentage value of this property now refers to the **background positioning area**, which I interpret as similar to the **containing block**, but with an addition of these 3 factors:

* Block with only content (`content-box`)
* Block with content and padding (`padding-box`)
* Block with content, padding and border (`border-box`)

The 3 factors are given by the `background-origin` property. **You can read more on [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/background-origin).**

Example:
{% codepen https://codepen.io/khangnd/pen/rNwLdMZ %}

In this example,

* The **parent** div has a size of **6x4**.
* The **child** div has a size of **3x2**, no `padding`, no `border`
* I used a DEV logo (with the ratio of a square **1:1**) as a `background-image` for the **child** div, with the `background-size` property set to **50% 50%**

The result is that the background image has been stretched to have a size of **1.5x1**, corresponding to 1/2 size of the **child**.

### `background-position`

Similar to `background-size`, the percentage of the `background-position` property also relies on the **background positioning area**.

Example:
{% codepen https://codepen.io/khangnd/pen/JjJKLaP %}

In this example, the same image and layout was used as the previous. As we changed the value of `background-position`, some observations we can see:

* Without any value (by default, the value is `0 0`), the background image is positioned at the **top left** corner.
* With `background-position: 0 50%`, the background image is positioned **left center**.
* With `background-position: 50% 50%`, the background image is positioned in the **center**.
* With `background-position: 100% 100%`, the background image is positioned **right bottom**.

> **Note**: `background-position: 0 50%` is equivalent to:
>
> * `background-position-x: 0`
> * `background-position-y: 50%`

Apparently, there are some calculations behind the percentage of this property, instead of just the distance between the image's top and left edges to the **child**'s. Through some researching and testing, it appears that the `background-position` property relies on the following calculation before yielding an actual value:

> offset X = (container's width - image's width) * background-position-x
>
> offset Y = (container's height - image's height) * background-position-y

In this case, with

* *container* as the **child** div
* *image's width/height* is the resulting size of `background-size`

### `font-size`

For `font-size`, the percentage value solely refers to its direct **parent block**.

Example:
{% codepen https://codepen.io/khangnd/pen/MWoeXMO %}

In this example, I use the same layout as the very first example, with the `font-size` assigned as below:

* **13px** for **grandparent**
* **26px** for **parent**
* **50%** for **child**

As a result, we can clearly see that the `font-size` of the **child** is now equivalent to the **grandparent** and is 1/2 of the **parent**, ignoring the fact that the `position: relative` is assigned to the **grandparent**, not the **parent**.

### `line-height`

While might not be as popular, I also mention this property as it also supports percentage. The percentage value of `line-height` relies on the `font-size` of **itself**.

Example:
{% codepen https://codepen.io/khangnd/pen/oNwLMXR %}

In this example,

* The paragraph has **11** lines
* `font-size` is set **20px**
* `line-height` is set **150%**

The actual `height` of the whole block is **~329px**,

* The `line-height` in this case is: 20 * 150% = **30px**.
* The `height` is then: 30 * 11 = **330px**, approximate to the actual `height`.

## Takeaways

Hope the article cleared up some of your understanding regarding the **percentage value** in CSS instead of making things worse 😅

I also tweeted a cheatsheet to summarize what is written so far, maybe it will come in handy to remember the meanings at first:

{% tweet 1433999937365164032 %}

And this is a collection of all the examples in the article: https://codepen.io/collection/xKwgdW

## References

* [Containing block (MDN)](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block)
* [Percentage (MDN)](https://developer.mozilla.org/en-US/docs/Web/CSS/percentage)
* [Background position's percentages (MDN)](https://developer.mozilla.org/en-US/docs/Web/CSS/background-position#regarding_percentages)
* [What are CSS percentages? - Jim Fisher](https://jameshfisher.com/2019/12/29/what-are-css-percentages/)
* [What does 100% mean in CSS? - Amelia Wattenberger](https://wattenberger.com/blog/css-percents)