Skip to content

Modular scale

Music is fundamentally a mathematical exposition, and when we talk about the musicality of typesetting it is because typesetting and music share a mathematical basis.

We’re sure you will have heard of concepts like frequency, pitch, and harmony. These are all mathematically determinable, but were you aware that perceived pitch can be formed of multiple frequencies?

A single musical note, such as one produced by plucking a guitar string, is in itself a composition. The different frequencies (or harmonics) together belong to a harmonic series. A harmonic series is a sequence of fractions based on the arithmetic series of incrementation by 1.

1,2,3,4,5,6 // arithmetic series
1,½,,¼,,// harmonic series

Representation of harmonious frequencies creating a symmetrical visual form

The resulting sound is harmonious because of its regularity. The fundamental frequency is divisible by each of the harmonic frequencies, and each harmonic frequency is the mean of the frequencies either side of it.

Visual harmony

We should aim for harmony in our visual layout too. Like the sound of a plucked string, it should be cohesive. Given we’re working predominantly with text, it’s sensible to treat the line-height as a basis for extrapolating values for white space. A font-size of (implicitly) 1rem, and a line-height of 1.5 creates a default value of 1.5rem. A harmoniously larger space might be 3rem (2 ⨉ 1.5) or 4.5rem (3 ⨉ 1.5).

Creating a sequence by adding 1.5 at each step results in large intervals. Instead, we can multiply by 1.5. The result is still regular; the increments just smaller.

1 * 1.5; // 1.5
1.5 * 1.5; // 2.25
1.5 * 1.5 * 1.5; // 3.375

This algorithm is called a modular scale, and like a musical scale is intended for producing harmony. How you employ it in your design depends on what technology you are using.

Custom properties

In CSS, you can describe a modular scale using custom properties and the calc() function, which supports simple arithmetic.

In the following example, we divide or multiply by the set --ratio custom property (variable) to create the points on our scale. We can make use of already set points to generate new ones. That is, var(--s2) * var(--ratio) is equivalent to var(--ratio) * var(--ratio) * var(--ratio).

:root {
--ratio: 1.5;
--s-5: calc(var(--s-4) / var(--ratio));
--s-4: calc(var(--s-3) / var(--ratio));
--s-3: calc(var(--s-2) / var(--ratio));
--s-2: calc(var(--s-1) / var(--ratio));
--s-1: calc(var(--s0) / var(--ratio));
--s0: 1rem;
--s1: calc(var(--s0) * var(--ratio));
--s2: calc(var(--s1) * var(--ratio));
--s3: calc(var(--s2) * var(--ratio));
--s4: calc(var(--s3) * var(--ratio));
--s5: calc(var(--s4) * var(--ratio));
}

We explicitly map out the ratio like this, mainly because Microsoft Edge (pre-Chromium) doesn’t get on well with custom properties that are based on other custom properties that use calc(). Arguably, it is actually much easier to visualize how the ratio works—especially for a developer that is new to your project.

Squares of increasing size, using a factor of 1.5, are placed next to each other. A curved line connects their top left corners

Image caption: Note the curved incline observable when connecting the top left corners of squares representing points on the scale

JavaScript access

Our scale variables are placed on the :root element, making them globally available. And by global, we mean truly global. Custom properties are available to JavaScript and also “pierce” Shadow DOM boundaries to affect the CSS of a shadowRoot stylesheet.

JavaScript consumes CSS custom properties like JSON properties. You can think of global custom properties as configurations shared by CSS and JavaScript. Here’s how you would get the --s3 point on the scale using JavaScript (document.documentElement represents the :root, or <html> element):

const rootStyles = getComputedStyle(document.documentElement);
const scale3 = rootStyles.getPropertyValue('--s3');

Shadow DOM support

The same --s3 property is successfully applied when invoked in Shadow DOM, as in the following example. The :host selector refers to the hypothetical custom element itself.

this.shadowRoot.innerHTML = `
<style>
:host {
padding: var(--s3);
}
</style>
<slot></slot>
`
;

Passing via props

Sometimes we might want our custom element to consume certain styles from properties (props) — in this case a padding prop.

<my-element padding="var(--s3)">
<!-- Light DOM contents -->
</my-element>

The var(--s3) string can be interpolated into the custom element instance's CSS using a template literal:

this.shadowRoot.innerHTML = `
<style>
:host {
padding: ${this.padding};
}
</style>
<slot></slot>
`
;

But first we need to write a getter and a setter for our padding prop. The || var(--s1) suffix in the getter’s return line is the default value. Use of sensible defaults makes working with layout components less laborious; we’re aiming for convention over configuration.

  get padding() {
return this.getAttribute('padding') || 'var(--s1)';
}

set padding(val) {
return this.setAttribute('padding', val);
}

Enforcing consistency

This padding prop is currently permissive; the author can supply a custom property, or a simple length value like 1.25rem. If we wanted to enforce the use of our modular scale, we would accept only numbers (2, 3, -1) and interpolate them like var(--${this.padding}).

We could check that a numeric value is being passed using isNaN():

if (!isNaN(this.padding)) {
console.error('<my-component>’s padding value should be a number representing a point on the modular scale');
return;
}

The modular scale is predicated on a single number, in this case 1.5. Through extrapolation—as a multiplier and divisor—the number’s presence can be felt throughout the visual design. Consistent, balanced design is seeded by simple axioms like the modular scale ratio.

Some believe the specific ratio used for one’s modular scale is important, with many adhering to the golden ratio of 1.61803398875. But it is in the strict adherence to whichever ratio you choose that harmony is created.