Doing Math and Logic in SCSS: 10 Code-Along Examples

Learn SCSS operators by running them. Ten copy-and-run examples covering derived values, scale systems, math.div, modulo patterns, unit handling, interpolation, comparisons and logic in mixins, and compile-time versus calc() boundary.

The SCSS operators article makes the promise plain: operators turn a stylesheet from a list of hand-calculated numbers into a system that derives them. Change one variable and every dependent value recomputes at compile time. This workbook runs the whole operator toolkit: the four arithmetic operations, the modern math.div, unit behaviour, interpolation, comparisons, logic in @if, and the finale that separates SCSS math from calc(). Each example is a complete styles.scss you compile yourself, with the resulting CSS shown so you can verify every computation.

Setup: run this once

Create a project folder with this index.html, which all examples share:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SCSS Operators</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header class="banner">SCSS operators at work</header>
<main class="content">
<div class="card">Card one</div>
<div class="card featured">Card two (featured)</div>
<div class="card">Card three</div>
</main>
<button class="btn">A button</button>
</div>
</body>
</html>

Then for each example, replace styles.scss and recompile with sass styles.scss styles.css. Add --watch to recompile automatically as you save.

1. Addition: building values from parts

Addition combines related quantities into one derived value, so the total is defined by its parts rather than by a number someone once calculated and everyone since has been afraid to touch.

$base-padding: 16px;
$extra-padding: 8px;
$border-width: 2px;
.card {
// the total is DERIVED, not hard-coded
padding: $base-padding + $extra-padding;
border: $border-width solid #1e2331;
// total vertical space the card consumes
margin-bottom: $base-padding + $border-width * 2;
}

Compiled CSS:

.card {
padding: 24px;
border: 2px solid #1e2331;
margin-bottom: 20px;
}

The compiled file contains plain numbers, because all the arithmetic happened at compile time, and that is the mental model for everything in this workbook: the browser never sees an operator. The win is maintenance. Change $base-paddingto 20px, recompile, and the padding and the margin both update in step, because they were defined as relationships rather than results.

2. Subtraction: accounting for what is taken

Subtraction expresses “the space that remains”: a content width after a gutter, a height after a header. Defining remainders this way means they survive every future resize.

$container-width: 960px;
$gutter: 24px;
$banner-height: 72px;
$viewport-height: 100vh;
.container {
width: $container-width;
}
.content {
// the room left after both gutters
width: $container-width - $gutter * 2;
// fill the screen minus the banner
min-height: calc(#{$viewport-height} - #{$banner-height});
}
.banner {
height: $banner-height;
line-height: $banner-height - 2px; // optically centre the text
}

Compiled CSS:

.container {
width: 960px;
}
.content {
width: 912px;
min-height: calc(100vh - 72px);
}
.banner {
height: 72px;
line-height: 70px;
}

Two subtractions with an instructive difference between them. $container-width - $gutter * 2 involves two compile-time constants, so SCSS resolves it to 912px on the spot. But 100vh - 72px mixes a viewport unit, whose real size only exists in the browser, with pixels, so it must travel inside calc() and be computed at render time, with #{} interpolation splicing the SCSS variables into the string. Knowing which subtraction belongs to which world is half the skill, and Example 10 makes the rule explicit.

3. Multiplication: scales and rhythm

Multiplication builds scale systems: a spacing scale from one base unit, a type scale from one ratio. One decision, multiplied outward, keeps every value on the same rhythm.

$space-unit: 8px;
$font-base: 16px;
$scale-ratio: 1.25;
.card {
padding: $space-unit * 2; // 16px
margin-bottom: $space-unit * 3; // 24px
font-size: $font-base;
}
.banner {
padding: $space-unit * 4; // 32px
// two steps up the type scale
font-size: $font-base * $scale-ratio * $scale-ratio;
}
.btn {
padding: $space-unit ($space-unit * 2); // 8px 16px
font-size: $font-base * $scale-ratio;
}

Compiled CSS:

.card {
padding: 16px;
margin-bottom: 24px;
font-size: 16px;
}
.banner {
padding: 32px;
font-size: 25px;
}
.btn {
padding: 8px 16px;
font-size: 20px;
}

Every spacing value is a multiple of the 8px unit and every font size a power of the 1.25 ratio, which is what designers mean by rhythm: the values relate instead of merely coexisting. The parentheses in the button’s shorthand padding matter, since they force the multiplication before SCSS reads the two values as a pair. One caution the article flags: multiply a length by a plain number, never by another length, because 8px * 2 is 16px but 8px * 2px is 16px squared, a unit no browser understands.

4. Division with math.div

Division distributes a total: a grid width across columns, a ratio from two lengths. Modern SCSS retires the / operator for this, because the slash already means something in CSS, and provides math.div instead.

@use "sass:math";
$grid-width: 960px;
$column-count: 4;
$gutter: 24px;
.content {
width: $grid-width;
}
.card {
// each column's share of the row, minus its share of the gutters
width: math.div($grid-width, $column-count) - $gutter;
// an aspect ratio is one length divided by another: units cancel
aspect-ratio: math.div(16, 9);
}

Compiled CSS:

.content {
width: 960px;
}
.card {
width: 216px;
aspect-ratio: 1.7777777778;
}

math.div(960px, 4) gives each card its 240px share, and subtracting the gutter leaves 216px, arithmetic you would otherwise do on a calculator and paste in as a magic number. The reason for the function is the slash’s day job: in font: 16px/1.5 sans-serif the / separates font-size from line-height, so SCSS could never be sure which meaning you intended, and old code using / for division now emits deprecation warnings. @use "sass:math" loads the module, and math.div says divide, unambiguously.

5. Modulo: the remainder operator

The % operator returns the remainder of a division, which sounds obscure until you need alternating patterns or to check whether a value divides evenly, where it is exactly the tool.

@use "sass:math";
$total-items: 7;
$per-row: 3;
// how many items sit in the final, possibly incomplete row?
$last-row-count: $total-items % $per-row;
.content::after {
content: "items in last row: #{$last-row-count}";
display: block;
font-style: italic;
}
// zebra striping computed with modulo inside a loop
@for $i from 1 through 4 {
.card:nth-child(#{$i}) {
@if $i % 2 == 0 {
background: #eef0f4; // even cards
} @else {
background: #ffffff; // odd cards
}
}
}

Compiled CSS:

.content::after {
content: "items in last row: 1";
display: block;
font-style: italic;
}
.card:nth-child(1) { background: #ffffff; }
.card:nth-child(2) { background: #eef0f4; }
.card:nth-child(3) { background: #ffffff; }
.card:nth-child(4) { background: #eef0f4; }

7 % 3 is 1, the remainder after two full rows of three, which answers layout questions like “does the last row need special centring”. The loop shows modulo’s most common styling job: $i % 2 == 0 is true for even numbers, so the cards alternate backgrounds, a compile-time zebra stripe. Pure CSS has :nth-child(even) for this simple case, but the SCSS version generalises to any stride, every third, every fourth offset by one, that CSS selectors express awkwardly.

6. Units and precedence: how SCSS keeps arithmetic honest

SCSS arithmetic understands units, converting compatible ones and refusing impossible combinations. Parentheses control the order of operations, exactly as in school algebra.

@use "sass:math";
// compatible units convert automatically
$width-a: 1in + 72px; // 1in = 96px, so 168px -> expressed in inches
// precedence: multiplication binds before addition
$wrong-feel: 10px + 5px * 2; // 20px, NOT 30px
$explicit: (10px + 5px) * 2; // 30px: parentheses decide
// units cancel in division, leaving a pure ratio
$ratio: math.div(320px, 16px); // 20, unitless
.card {
width: $width-a;
padding: $wrong-feel;
margin-bottom: $explicit;
// a unitless ratio is what line-height wants
line-height: math.div(24px, 16px);
}

Compiled CSS:

.card {
width: 1.75in;
padding: 20px;
margin-bottom: 30px;
line-height: 1.5;
}

Three behaviours worth internalising. Compatible units add, with SCSS converting 72px into 0.75in and answering in the first operand’s unit. Precedence follows mathematics, so 10px + 5px * 2 is 20px, and the article’s advice to parenthesise anything non-trivial is really advice about readability insurance. And dividing two lengths cancels the units, leaving the pure number 1.5, which is precisely what line-height prefers, a genuinely useful trick rather than a curiosity. Incompatible additions, like 10px + 2em, fail at compile time, which is SCSS catching a bug the browser would have swallowed silently.

7. String interpolation with #{}

The #{} syntax splices a variable’s value into a string, selector, or property, which is how you build class names, asset paths, and content from data.

$asset-path: "/images/icons";
$icon-name: "menu";
$breakpoint-name: "tablet";
$breakpoint-width: 768px;
.btn::before {
// build the class-like content string from parts
content: "icon-#{$icon-name}";
}
.banner {
// assemble a URL from a base path and a name
background-image: url("#{$asset-path}/#{$icon-name}.png");
}
// interpolation works in selectors and at-rules too
.hide-on-#{$breakpoint-name} {
@media (max-width: #{$breakpoint-width}) {
display: none;
}
}

Compiled CSS:

.btn::before {
content: "icon-menu";
}
.banner {
background-image: url("/images/icons/menu.png");
}
@media (max-width: 768px) {
.hide-on-tablet {
display: none;
}
}

Interpolation is string assembly: #{} evaluates whatever is inside and pastes the result into the surrounding text, which is why it works in places plain variables cannot go, selectors, media queries, and quoted strings. The three uses shown are the everyday ones: composing content values, building asset URLs from a base path so the path changes in one place, and generating named utility classes. The article’s caution applies: interpolate where a string must be built, and use plain variables everywhere else, since width: #{$w} works but is noise compared to width: $w.

8. Comparison operators in @if

Comparisons return true or false, and inside @if they let a mixin make decisions: different styles for different input sizes, with the logic living in one place.

@use "sass:math";
@mixin sized-text($size) {
font-size: $size;
@if $size >= 24px {
font-weight: 700; // big text carries its own weight
letter-spacing: -0.5px;
} @else if $size >= 16px {
font-weight: 400;
} @else {
font-weight: 400;
// small text needs room to breathe
letter-spacing: 0.3px;
line-height: 1.6;
}
}
.banner { @include sized-text(28px); }
.card { @include sized-text(16px); }
.btn { @include sized-text(13px); }

Compiled CSS:

.banner {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
}
.card {
font-size: 16px;
font-weight: 400;
}
.btn {
font-size: 13px;
font-weight: 400;
letter-spacing: 0.3px;
line-height: 1.6;
}

The mixin is one definition producing three different outputs, because the @if chain compares the incoming size against thresholds and emits only the matching branch. The branches are tested top to bottom and the first true one wins, the same ordering logic as any language’s conditionals, so the thresholds descend deliberately. This is the pattern that scales into design systems: encode the typographic judgement, large text tightens, small text opens up, once, and every caller inherits it.

9. Logical operators: and, or, not

andor, and not combine comparisons into compound conditions, which is how a mixin validates its inputs or handles configuration flags.

$dark-mode: true;
$high-contrast: false;
@mixin themed-card($width) {
// validate: sensible widths only
@if $width > 100px and $width < 600px {
width: $width;
} @else {
width: 300px; // a safe default for silly inputs
}
// either flag switches to the dark palette
@if $dark-mode or $high-contrast {
background: #1e2331;
color: #eceff4;
}
// borders only when NOT in high contrast
@if not $high-contrast {
border: 1px solid #6f7580;
}
}
.card { @include themed-card(280px); }
.featured { @include themed-card(2000px); } // out of range: gets the default

Compiled CSS:

.card {
width: 280px;
background: #1e2331;
color: #eceff4;
border: 1px solid #6f7580;
}
.featured {
width: 300px;
background: #1e2331;
color: #eceff4;
border: 1px solid #6f7580;
}

Three connectors, three jobs. and demands both conditions, which makes it the range check: a width must exceed 100px and stay under 600px, so the featured card’s absurd 2000px falls through to the default, a guard rail against typos in a big codebase. or accepts either flag, letting two settings share one outcome. not inverts, styling the absence of a mode. Note the words are spelled out, and rather than &&, one of SCSS’s small mercies for readability.

10. SCSS math versus calc(): compile time versus render time

The finale draws the boundary that runs under every previous example. SCSS math happens once, at compile time, between values SCSS knows. calc() happens in the browser, where viewport sizes and container widths actually exist. A fluid layout needs both.

@use "sass:math";
$sidebar: 260px;
$gutter: 24px;
$columns: 3;
.container {
// COMPILE TIME: all values known now -> a fixed number comes out
padding: math.div($gutter, 2);
}
.content {
// RENDER TIME: 100% depends on the browser -> calc() required
width: calc(100% - #{$sidebar + $gutter});
}
.card {
// both worlds in one line: SCSS computes the subtraction inside calc
width: calc((100% - #{$gutter * ($columns - 1)}) / #{$columns});
}

Compiled CSS:

.container {
padding: 12px;
}
.content {
width: calc(100% - 284px);
}
.card {
width: calc((100% - 48px) / 3);
}

Read the compiled output to see the division of labour. Where every operand was a compile-time constant, SCSS collapsed the expression to a number: 12px. Where 100% appeared, a value that means nothing until the browser measures the parent, SCSS computed everything it could, folding $sidebar + $gutter into 284px and $gutter * 2 into 48px, and left the rest as a calc() for the browser to finish. The rule of thumb the article lands on: SCSS math for relationships between your own design tokens, calc() for relationships involving the browser’s world, and #{} interpolation as the bridge that lets your tokens travel into it.

Work through these and you have the whole article in practice: derived values with addition and subtraction, scale systems with multiplication, math.div and why the slash retired, modulo for patterns, unit conversion and precedence, #{}interpolation, comparisons and logic inside mixins, and the compile-time versus render-time boundary that decides between SCSS math and calc(). The shift worth keeping is the first example’s: stop writing results into your stylesheets and start writing relationships, because relationships recompute themselves and results rot.

See you soon.

View Comments (2)

Leave a Reply

Subscribe to My Newsletter

Subscribe to my email newsletter to get the latest posts delivered right to your email. Pure inspiration, zero spam.

Discover more from Discuss Data Science, Machine Learning and Analytics

Subscribe now to keep reading and get access to the full archive.

Continue reading