Using CSS typed arithmetic
CSS typed arithmetic refers to permitted calculations performed with typed CSS values via functions such as calc()
, and specifically, to the behavior defined in the CSS values and units module. CSS typed arithmetic enables dividing a value with one unit by a value with a different unit of the same data type, resulting in unitless quotients.
These quotients can then be used as a <number>
in the values of unitless properties, or converted into any numeric data type (such as a <length>
, <percentage>
, or <angle>
) by multiplying them by a numeric typed value.
This typed arithmetic behavior enables relationships to be created between different values on a page. This article explores typed arithmetic and presents several examples that make use of it.
Note: Originally, the CSS values and units module limited the arguments multiplication and division operations could take to make dividing by zero detectable at parse time and avoid issues such as squared units. Typed arithmetic relaxes some of those restrictions in supporting browsers.
CSS typed arithmetic rules
When performing calculations on values in CSS, there are some rules around compatibility of values with different data types.
Addition and subtraction
When adding or subtracting values, all the values must be within the same overall data type. The following examples all produce valid results:
/* <length-percentage> units */
calc(250px - 150px)
calc(100% - 50px)
calc(50vw + 2rem)
calc(25cqw + 3in)
/* <angle> units */
calc(40deg + 2rad)
calc(420deg - 1turn)
However, the following are invalid. The results of such mixed-type calculations would be meaningless:
calc(200px + 100ms)
calc(50% + 90deg)
Multiplication
When multiplying values in CSS, only one of the values can have a unit. All other values must be unitless <number>
values. This is because you want a product that is a greater or lesser amount of the same unit, and do not want to create squared units, which no CSS properties accept.
calc(200px * 4) /* 800px */
calc(60deg * 3) /* 180deg */
If you try to multiply two values with units — even the same units — normal mathematical rules dictate that the units should be multiplied as well. For example:
calc(200px * 4px)
In this case, the function is invalid as 800px2
is meaningless in CSS.
Division
When dividing values in CSS, you can divide a value with a unit by a unitless value:
calc(1000px / 2) /* 500px */
calc(360deg / 4) /* 90deg */
However, you can't divide a unitless value by a value with a unit as that doesn't make sense:
calc(1000 / 2px) /* ?!? */
When a value of any numeric data type is divided by a value of the same type, the units cancel each other out, and you are left with a unitless value. Behind the scenes, the two values are computed before being divided by one another.
As a result, the same calculation can have very different results depending on the context it is used in and the units of the divisor.
Take the following example:
calc(100vw / 1px)
The 100vw
is equal to 100%
of the width of the viewport. If the viewport is currently 1000px
wide, the calculation will return the unitless value 1000
. However, if the viewport is 500px
wide, the calculation will return the unitless value 500
.
If we replace the 1px
divisor by 1em
, we get very different results:
calc(100vw / 1em)
If the viewport is 1000px
wide, and 1em
is equal to the browser default of 16px
at the point of calculation, the previous calculation will return 1000px / 16px
= 62.5
.
It used to be the case that you couldn't divide one typed value by another, even if the two values have units of the same type. However, the specification was updated to permit this; check for browser compatibility.
Why is the behavioral update useful?
Being able to divide one typed value by another of the same type may not sound very significant at first, but it enables useful associations between different values that can be used to create responsive UI features.
The key to all of this is being able to represent values in a unitless form via divisions like this one:
--viewport-width-in-pixels: calc(100vw / 1px);
The result is a <number>
representing the viewport width in pixels as a unitless value. This can be used anywhere a number is valid, including other calc()
functions. You can dynamically vary other property values based on this value, regardless of what units they have.
For example, the unitless value can be immediately transferred to opacity
:
opacity: calc(var(--viewport-width-in-pixels) / 1000 - 0.5);
You can multiply it by a value such as 1deg
to create an <angle>
value:
rotate: calc(var(--viewport-width-in-pixels) * 1deg);
You can multiply it by a value such as 1rem
to create a <length>
value:
font-size: calc(var(--viewport-width-in-pixels) * 1rem / 200);
Let's walk through some examples to show how CSS typed arithmetic can be useful.
Basic example: responsive background opacity
The responsive background opacity example (see source code) shows how to vary the opacity of a background image as the viewport width is changed. Having the background image fade out as the screen gets narrower means the image is less likely to affect the readability of the text content as they start to overlap.
HTML
The HTML contains some basic text content wrapped in a <div>
element.
<div class="wrapper">
<h1>Prose of the century</h1>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla luctus
aliquam dolor, eu lacinia lorem placerat vulputate. Duis felis orci,
pulvinar id metus ut, rutrum luctus orci. Cras porttitor imperdiet nunc, at
ultricies tellus laoreet sit amet.
</p>
</div>
CSS
We start by defining a CSS custom property called --width-percentage
on the :root
element, which contains the result of the calculation 100vw / 2000px
. This value represents the viewport width as a percentage of 2000px
that we will later use as an alpha channel value. When the viewport is 2000px
wide, the calculation will return 1
, which is equivalent to 100%
alpha. Anything less than a 2000px
viewport width will result in a smaller value.
:root {
--width-percentage: calc((100vw / 2000px));
}
Note:
Any alpha value greater than 1
is treated as 1
, therefore we don't need to clamp the maximum value.
We then set a fixed width
and some padding
on the wrapper <div>
, and center it horizontally using margin
.
.wrapper {
width: 480px;
padding: 20px;
margin: 0 auto;
}
Finally, we set multiple backgrounds on the <body>
element. We'll go through these in reverse order:
- The last and therefore bottom-most
background
value is a non-repeating background image located near the top-right corner. - The top background is a white semi-transparent overlay created using a
linear-gradient()
function with both color stops set to the same color. The color's alpha channel value is calculated as1
minus the--width-percentage
custom property we set earlier: As the viewport width gets narrower,--width-percentage
will get smaller, therefore the white gradient will become more opaque, reducing the opacity of the background image.
body {
background:
linear-gradient(
rgb(255 255 255 / calc(1 - var(--width-percentage))),
rgb(255 255 255 / calc(1 - var(--width-percentage)))
),
url("https://mdn.github.io/shared-assets/images/examples/colorful-heart.png")
no-repeat top 50px right 50px;
}
Result
To see the result, view our responsive background opacity example live (see source code). Try altering the viewport width to see how the background opacity varies as a result.
Varying values of different types based on a single value
The different type variations example (see source code) works in a very similar way to the previous example, except that in this case, we demonstrate the use of a single variable value in the definition of multiple other values with different data types.
Specifically, we set a paragraph's background color and font size based on the viewport width.
HTML
The markup for this example contains a paragraph of text:
<p>Hello there!</p>
CSS
We start off our CSS in a similar way to the previous demo, creating a unitless value based on the viewport width. This time, we calculate it by dividing 100vw
by 1px
and store the resulting number in a custom property called --viewport-in-pixels
. The value represents the current viewport width in pixels, without any units.
:root {
--viewport-in-pixels: calc(100vw / 1px);
}
Now on to the styling of the paragraph itself. We first give it some basic styles (a border
and text-align
of center
), then set two values based on the --viewport-in-pixels
property created earlier:
- We set the
font-size
of the paragraph to a value equal to--viewport-in-pixels
divided by200
, multiplied by1em
to convert the numeric quotient intoem
s. - We set the
background-color
of the paragraph to anlch()
color value. The lightness and chroma components are constant values (75%
and50%
, respectively), whereas the hue component is set to--viewport-in-pixels
divided by10
, plus100
. We then multiply the result of that by1deg
to ensure the value is an<angle>
.Note: This last step is not strictly necessary, as
lch()
also accepts unitless hue values. However, a degree value may be more intuitive, and we wanted to show another example of how the unitless value can be converted into a different data type.
p {
border: 5px solid black;
text-align: center;
font-size: calc(1em * (var(--viewport-in-pixels) / 200));
background-color: lch(
75% 50% calc((100 + (var(--viewport-in-pixels) / 10)) * 1deg)
);
}
Result
To see the result, view our different type variations example live (see source code). Try altering the viewport width to see how the paragraph font size and background color both vary as a result.
An animated story circle
The animated story circle example (see source code) demonstrates a more complex effect created by leveraging CSS typed arithmetic rules. In this case, the different paragraphs of a body of text are laid out in a circle emanating from a center point. What's more, the circle of paragraphs closes up into a fan shape when the viewport is made narrower, and opens up again when it is made wider.
HTML
The HTML is fairly basic — a <div>
containing multiple <p>
elements. The text of the first paragraph is wrapped in a <strong>
element to provide an indicator of where to start reading.
<div class="story-circle">
<p><strong>Hello there!</strong></p>
<p>This is</p>
<p>quite an</p>
<p>interesting way</p>
<p>to tell a</p>
<p>story</p>
<p>all around in</p>
<p>a circle.</p>
<p>What fun!</p>
</div>
CSS
We start the CSS by setting a height
of 100%
on the :root
element.
:root {
height: 100%;
}
Next, we set several properties on the <body>
element:
- We start by setting a
height
ofinherit
, meaning the<body>
will inherit the:root
element's100%
height and therefore span the full height of the viewport. - Next, we horizontally center the
<body>
usingmargin
, and give it amax-width
. As you'll see later on, this upper bound is important for controlling the maximum rotation of the fan/circle shape. - We center the
<div>
withclass="story-circle"
horizontally and vertically inside the<body>
using flexbox. - We use the
container-type
property to declare the<body>
as an inline size query container. This is important because we want to vary the shape rotation based on the<body>
width, and not the viewport width as we did in previous examples. Setting it as a size query container allows us to reference its size in calculations.
body {
height: inherit;
margin: 0 auto;
max-width: 1600px;
display: flex;
align-items: center;
justify-content: center;
container-type: inline-size;
}
We now come to styling the story-circle
<div>
. We set its width
and height
to 1px
: it will act as a reference point with its child paragraphs positioned in a circle around it. (We don't even need to set positioning on it, as it is fine for the paragraphs to be positioned relative to the <body>
).
We then create a custom property called --width-percentage
that contains the result of 100cqw
(100% of the width of the element's parent query container, which is the <body>
element) divided by 1200px
, minus 0.33333
. This is the key value that wll control the amount the circle rotates by as the viewport width is changed.
.story-circle {
width: 1px;
height: 1px;
--width-percentage: calc((100cqw / 1200px) - 0.33333);
}
We are using the container query width rather than the viewport width to control the rotation because we want to set a maximum bound for the container, which is controlled by the <body>
max-width
. If the rotation value becomes greater than a full circle, the last paragraph will start to overlap the first one, which spoils the effect.
Since the max-width
is 1600px
, you might have expected the calculation to be 100cqw / 1600px)
. This would work, but we've instead gone for (100cqw / 1200px) - 0.33333
(the 0.33333
comes from 1600px/1200px - 1
). Both would cause the maximum rotation to occur at a <body>
width of 1600px
, but the fan now has a smaller minimum rotation, which gives a better effect at narrow viewport widths.
The final style rule selects the paragraphs themselves. Most of this styling is rudimentary. It is worth pointing out that we have set position
to absolute
to cause all of the paragraphs to sit on top of one another, and we've set a transform-origin
value of center left
to cause paragraphs to rotate around the center of their left edge, so they will all fan out from a center point over their containing <div>
.
Now onto the interesting bit — we define a custom property called --angle
that contains the unitless rotation angle of the paragraph, before setting the rotate
property to the resulting value. We multiply the resulting number by 1deg
to convert it to a degree value. The --angle
custom property is the product of three values:
- The paragraph's
sibling-index()
minus1
, which causes the first paragraph to have a rotation angle of0
, as we want it to be horizontal. 360
divided by the paragraph'ssibling-count()
, which causes all the paragraphs to be equally spaced around the circle, meaning the design will still work if the number of paragraphs changes.- our
--width-percentage
custom property, which causes the rotation of the paragraphs around the circle to vary as the viewport width changes. Remember that this property has a maximum value of1
, which will be achieved when the<body>
element hits itsmax-width
of1600px
.
p {
padding: 10px;
width: 150px;
text-align: right;
background: linear-gradient(to right, red, orange 50%);
border-radius: 5px;
position: absolute;
transform-origin: center left;
--angle: calc(
((sibling-index() - 1) * (360 / sibling-count())) * var(--width-percentage)
);
rotate: calc(var(--angle) * 1deg);
}
Result
To see the result, view our animated story circle example live (see source code). Try increasing and decreasing the viewport width to see how the paragraph fan shape unfolds into a circle and back again.
See also
calc()
,abs()
- CSS values and units module
- CSS Typed Arithmetic on css-tricks.com (2025)