Normally, a project will have a set of pre-determined font sizes, usually as variables named in such a way that seeks some semblance of order and consistency. Any project of considerable size can use something like that. There are always headings, paragraphs, lists, etc. You could set font sizes explicitly and directly everywhere (e.g. font-size: 18px
). Raw CSS, as it were. I do see that occasionally — mixing not just sizes but also units like px
, rem
and em
in mindless chaos.
That’s why the CSS of a project typically uses variables or mixins — we’re shooting for structure, maintainability and, ultimately, consistency. We all know naming is hard and it doesn’t take looking much further than naming font size variables to see why. How should we name a small font size variable so it’s clear that it’s smaller than a large font size variable? And what happens if we need to insert a new variable in between them — is that one named in a way that clearly explains its relationship to the other size variables?
We’ll continue talking about naming font size variables in this post. But, really, the issue extends beyond font sizes to any sort of size or length value. Think paddings, margins, widths, heights, border radii, etc. These things need structure and consistency, too!
How do you define font sizes in your project? Does it look something like this with custom variables:
:root {
/* Font size variables */
--small: 12px;
--medium: 16px;
--large: 24px;
}
Or perhaps in Sass (which is what we’ll be using throughout this article) you might have variables for $small
, $medium
, and $large
font sizes.
Fine. After a while, let’s say the designer adds a new <h1>
heading for a hero section. And it is very large. Larger than anything you have in the project. No problem, you reply. You add an $xlarge
to the project, and go about your day.
The following day, the designer makes a nice form label, which again has a new font size. This new size, though, is larger than small, but smaller than medium.
Here we go.
What should you call it? $small-medium
? $small-2
? $smedium
? Whatever you name it, you won’t be happy with it. Because there is no word for that.
Or should you maybe refactor it? Create a new $xsmall
, and change all instances of $small
to $xsmall
? And then you can use $small
for the form label? There’s a small risk that you will forget to change somewhere and, hey, presto: a bug. What happens next time, when something is introduced that has a larger size than the $medium
variable value? Do we have to refactor $large
and $xlarge
too?
I suggest adhering to a scale, always. An easy fix would be to further abstraction, perhaps ditching numbers and sizes in favor of functional names, like $form-label
instead of $small-2
or $xsmall
.
But imagine having a set of font sizes like this:
$small: 12px;
$form-label: 14px;
$medium: 16px;
$large: 24px;
That is a broken scale. It’s a size concept and a component concept mixed together. It raises questions. Should an alert or a button be allowed to use $form-label
? Yuck.
Maybe you have a Greek thing going on, naming the variables $alpha
, $beta
, $gamma
? Let me ask you then, what is then bigger than $alpha
? $alpha-large
? Or wait, is $alpha
the small one?
I have also seen names like $button-font-size
, $label-font-size
, $blockquote-font-size
. That seems to me like one variable per element used, instead of a scale, and sounds like it could be a lot of duplicated code if the same value is being used in multiple places, but with different names.
Perhaps you’re working with one base font size and percentages only? Sure, but I would say you need variables for the percentages. That’s how Geoff handles font sizing and even he admits that the setup raises his own eyebrows. Calculations with subjectively-named variables might be clear to you, but crazy-looking and complicated for anyone else jumping into the project.
h1 {
font-size: clamp(var(--text-size-large), calc(var(--text-size-base) * var(--text-size-scaler)), var(--text-size-huge));
}
We need a better system
Adding and removing stuff constantly is the way we want to work. This is modern day development — MVP, Agile, and all the other hot buzzwords.
What we need is a scale syntax that allows room for changes. Adding a new size to the scale should be easy without introducing breaking changes. I’m thinking of a kind of scale that is both flexible and infinite. It must be more sophisticated than $small
, $medium
and $large
.
It should be also be descriptive and intuitive. Preferably, you shouldn’t have to look up the variable names in the settings file or the config, or wherever you store these things. I don’t have the slightest clue if $epsilon
comes before or after $sigma
. Do you?
Using existing systems
Before trying to invent something new, is there an existing syntax or system we can leverage? Here are a few I’ve encountered.
International system of units
Surely, you’re familiar with terms like “kilobyte” and “megabyte.” Europeans are very used to “millimeter” and “centimeter.” Other examples are “giga,” “tera,” and “peta.” These prefixes can be used for length, weight, volume and more. Could a $centi
font size work? It is intuitive to a certain extent, that is, if you’re familiar with the metric system. This is a finite scale. And there’s no room to add new sizes because they are already set.
Traditional point-size names
Long before computers and desktop publishing, books and newspapers were printed with lead type. The type setters had different names for different sizes. The sizes have a reference to a point size (pt
) and could, in theory, be used for pixel sizes (px
).
The type sizes in this system are called “Nonpareil,” “Pica,” “Cicero,” and “Great Primer,” just to name a few. The names are different depending on continent and country. Plus, the same name can have different sizes, so… quite confusing.
That said, I do like this system in a way because it would be like paying respect to an old craftsmanship from times past. But the names are so weird and specifically meant for type sizing, that it feels like a stretch to use for things like breakpoints and spacing.
Placing everyday objects on a scale
How about using stuff from our everyday life? Say chili peppers.
There are many kinds of chili peppers. The $habanero
, is hotter than the $cayenne
, which is hotter than the $jalapeno
. That would be fun, yeah?
But as much as I enjoy the idea of writing font-size: $tabasco
, I see two problems. If you’re not into peppers, you cannot know which pepper is hotter than another pepper — so, it’s not universally intuitive. Also, the bell pepper is 0 on the Scoville scale, and nothing is below that. Carolina Reaper is the hottest pepper in the world, so the scale is finite.
And yeah, peppers scale-wise are not larger or smaller, they are hotter. Bad concept. Maybe something more common, like types of balls?
There‘s a large range of different kinds of balls. You have handballs, soccer balls, volleyballs, etc. Need something larger than a medicine ball? Use $beach
. Something smaller than a tennis ball? Use $pingpong
. This is very intuitive, as I’d imagine everyone has played with all sorts of balls at some point, or at least are familiar of them from sports.
But is a ping pong ball larger than a golf ball? Who knows? Further, a bowling ball and a soccer ball are actually the same size. So… again, not perfect.
Scaling up to planets could work, but you would have to be knowledgeable in astronomy.
How about straight-up numbers? We’re unable to use numbers alone because tools like stylelinter will protest. But would something like this work:
$font-14: 14px;
$font-16: 16px;
$font-24: 24px;
Well, it’s infinite as there is always room for new additions. But it’s also incredibly specific, and there are some downsides to have the actual value be part of the name like that. Let’s assume that $font-18
is used in a lot of places. And now, they say, all places with 18px
must be changed to 19px
(because reasons). Now we need to rename the variable from $font-18
to $font-19
then change the value of $font-19
from 18px
to 19px
. And that’s before we finally update all places using $font-18
to $font-19
. This is almost like using raw CSS. Low score for maintainability.
What about the animal kingdom?
Mother Nature has provided a myriad of species on this earth, which comes in handy in this situation. Imagine something like this:
$mouse: 12px;
$dog: 16px;
$hippo: 24px;
Need something smaller than a mouse? Use $bee
, $ant
or $flea
. Larger than a bear? Try $moose
or $hippo
. Larger than an elephant? Well, you have the $whale
, or heck, we can go prehistoric and use $t-rex
. There’s always an animal to squeeze in here. Very versatile, very intuitive, also infinite (almost). And fun, too — I wouldn’t mind doing font-size: $squirrel
. 🤩
But then again, even that might require needing to reference the variables, unless we know exactly which animals are contained in our zoo of font sizes. But maybe that’s not a big deal as long as it scales.
I have spent way too much time pondering this
Or have I? The code base is where you spend your working hours. It’s your work environment, just like chairs and monitors. And the workplace should be a nice place.
How do you handle your font size scales? Do you have one system for fonts and another for things like margins? Can anyone jump right into your code and understand how everything is organized? Please tell in the comments!
Yeah, I’ve been thinking about the same issue last year.
For numerical values, I just include the number in the name.
Or to avoid magic numbers, just use typographical scale so the numbers in a variable have a meaning. Also, having a vertical rhythm helps in the same way for spacings.
For colors, I use keywords like
primary
,secondary
,tint
,accent
, etc.Great writeup and comparison. Just goes to show how complex this issue is!
I’ve definitely caught myself using
--small
then--smallest
and--smaller
, it’s a slippery slope!In an ideal world we could use expansive naming systems and remember them all while coding, but I think we’re all going to be looking at the definitions a lot anyway, whether it’s for colours, font names, etc.
Maybe there’s too much anti-semantic-thought here? “Big” is different from H1, H1 could be smaller than H2, or be context-specific.
As a type person and not a coder, everything becomes a percentage of body copy size, which is the only fixed value.
H1-H6 is always large to small, and if for example you need variants of H1 for special occasions it becomes H1-large or H1-landing or whatever, not really many of these.
Just like captions or legal text, they are rare exceptions and the name makes enough sense to guess they will be small, not large. So naming by use case makes enough sense for me.
you could look to music nominclature for demitones, and use $smallSharp, $mediumFlat (possibly even $bMedium), or (my preference) $demiMedium to place an item between $small and $medium
$subMedium (but not $superSmall — that’s confusing), $semiSmall, $lesserMedium, $greaterSmall, $underMedium, and $greaterSmall also come to mind, though not as useful.
This would fit well with the modular scale. Most likely there would be a lot to take from music and just apply it in CSS as a whole structure/rules.
I think in general, a site should only use a finite (handful) amount of font sizes anyways, so the list of names does not need to be very long. One idea would be using the
font-weight
scale for type, as in$size-100
is smallest,$size-900
is largest, and you have everything in between available to use. Need to add a size between100
and200
, try150
.I was just commenting to make this exact case!
Andy Bell’s Cube CSS introduced me to this and is great for giving an instantly obvious scale relative to a ‘base’ of 400.
I personally like using a strict typographic scale (like those generated by utopia) on multiples of 100, and if I need ad-hoc sizes place them in a reasonable place between.
I do like it, I’ll try it.
Same case for colors, where you palettes are from “green-100” to “green-900”, and in the remote case you need a shade it’s not in there (or very close to one…), you can always go with “green-150”.
Hi! UX/Ui designer here. Designer can and should resolve this issue. Very communicated teams tend to make this issue known to all the teammates and this changes should be based on the growing design system. But designers aren’t that unruling ungovernable creatures. They need to be standardized or at the very least, consistent.
Now that, if you are a lone wolf full-stack developer, working sporadically with designers, you better let them know or you’ll become crazy.
This is usually how I end up doing it. I twist the designers arm so they actually make a typography ref sheet, and tend to use the names they come up with. Title, Headline, Subheading, Body, Lead, Caption, etc.
Then again, half the time they come up with really lazy names like “large bold”, and I’m back to improvising.
Not the most intuitive, but I tend to use ‘smaller’ < ‘small’ < ‘smallish’, with similar on the ‘big’ end to give a few extra points to my scale. For larger projects, I can have a ‘tiny’ and ‘huge’ set, though I never need to.
Are we going the wrong way here? Shouldn’t the size reflect the meaning?
Say you start with normal-text; headline; sub-headline; footnote; citation etc. you can always add big-headline and small-headline and tiny-footnote and everyone knows what they are.
I like this method actually.
Not really. Because it’s easy that the size of a “headline” is different in the homepage, than in the legal policy page, than in the product page. Won’t work for me.
I use typographic scale variables that are numerically relative but don’t represent pixel sizes. My base text size is
--scale0
. One size smaller than the base size is--scale-1
. One size larger than the base size is--scale1
. Here’s what a five-step scale might look like, from smallest to largest:I explain in more detail in this 24 Ways article.
A scalable size nomenclature system as suggested here is certainly a useful endeavor. However, designers should be held to a higher standard; more decisive (e.g. fewer size delineations) and/or more deliberate (e.g. rational scalars). The primary effect is improved visual hierarchy/UX and the secondary effect is replicable logic for developers.
I spent some time pondering this a while ago, and came up with a system. It’s not perfect, but addresses some of the issues mentioned here. The problem I initially tried to solve was that I didn’t like the fact that small and large are spectres, but medium is just deadeye in-between. Also extra/x doesn’t imply any direction on the spectre, e.g. xmedium makes no sense. So I tried using minus and plus, represented by m and p, and instead of applying multiple m’s and p’s I just use numbers. Which would look something like this (I use “regular” as common in font naming, but “medium” would be fine too):
small-m2
small-m
small
regular-m
regular
regular-p
large
large-p
large-p2
…which would be the equivalence of:
xxxsmall
xxsmall
xsmall
small
medium
large
xlarge
xxlarge
xxxlarge
One advantage is that you still have the basic small, regular (or medium) and large, and can expand on those. You could even add something in between, if in a pinch, with something like large-p1-2. I also think that suffixes look a bit more clean than prefixes.
I demand from my designers to make a styleguide initially and stick to it until redesign. :) I usually ask them to define headers h1-h6, paragraphs, links, labels. After that, I set a system with $font-size-xxs up to $font-size-xxl usually setting paragraph as $font-size-normal and skipping every other size.
I was in this mess even I started my life as a UI developer but soon found a hefty solution that does everything for me.
I start with a base $fontSize, then move upwards like $fontSizeLarge and then $fontSizeLarger and then $fontSizeLargest, and you can basically do the same for the smaller ones. If you need more, add kilos and centis and you’ll have a flexible mess of variables.
I use component context for most things and utility naming for anything else:
--hero-heading
--fs-24
This gives me both the readability and maintianability I want.
I would go with a bunch of variables : from xs to xl, then go raw for specific needs. Because it is already too complicated like this ^^
At least I do it like that for margins and paddings.
And in fact, I tend to use calc function on basic variables for specific needs…
And I rearrange everything on every new project… so…
I’ve stopped using variables for font-size, aside from the main body across layout-wide breakpoints. For example:
Aside from that, I use direct values in CSS classes/components, because variables for font sizes don’t bring any additional consistency or maintainability benefit.
Not specifically for font sizes, but it could be fore anything where there is a potential for in-between values: a 100 point scale. Or whatever scale makes sense for the thing you’re doing.
Whether you define variables or class names, assign the smallest/least thing to
--font-1
, and assign the largest/most thing to--font-100
. Then your values act sorta like percentages, in terms of the relationships between things, not actual values. If you need something larger than 100, saying it’s the “200%” size is fine. And to prevent under-run, pick the zero value to be so small that you would never use it anyway.By doing this, you get as much room as you need in between existing defined values, and you can also guestimate how big something will be relative to other things, without needing to know what the actual values are at any given time. You can also change the underlying values without worrying about renaming things. Even linearity is not necessary, so long as
--thing-25
is smaller than--thing-30
.Here’s an idea – have all your typography scale named first and then pick the required tokens to your run-time
In run time I’ll need only
Not sure how much sense it makes but we’re keeping the predefined consts untouched even though in run-time the scale isn’t chronological
I use tailwind so the syntax i use is
–text-sm
–text-md
–text-lg
…
If a new font size come up later in the dev process, I will cry a little bit. Then i will just patch my vars.
–text-sm-bis
–text-sm-ter
Usually a good design doesn’t need more than 3/4 font sizes. So if the designer becomes crazy maybe he needs to be sent to a goulag.
I use standard xs, sm, md, lg scaling, if I have to put something between i just mix the letters like sm, smd, md. Need another between smd, smdd md yeah I hope You can see the logic here
I use Category/Type/Item (CTI) and outline levels for naming.
I don’t use such classes at all. My markup has nested article elements and headers and footers.
If I get a request to set nested footers to a specific size I may use selector such as
article article > footer { font-size: 0.8 rem; }
And if more things need the same size, I list those with that same block. As a result, I can change all those places at once if the font size needs to set up 0.9rem instead.
And I can also do multiple groups of selectors that currently happen to use the same font size but may go in different directions on the future (e.g. “small text”/footnote vs and article footers). Both may have 0.9rem now, but those are different contexts so I would put those in different selector groups. It’s more probable that footnotes have smaller than body text in future, too, whereas the style of footer is more like a trend.
In addition the accessibility is automatically better because the content isn’t just a collection of divs.
I use inc and dec suffixes
small_inc- larger than small
medium_dec – smaller than medium
I’ve been following Bootstrap’s lead for colors 100 to 900.
I think this would work for font-size too. font-size-500 could be regular with lots of room up and down. Need a new number: font-size-352 to the rescue.
One advantage of this is that people are unlikely to think font-size-200 is 200 points.
I think it’s abstracted enough that it might even work for designers.
I’ve also pondered this a bit.
What about using descriptive vars:
Along side functional vars:
Adding new descriptive or functional vars and reassigning would be straight forward.
I think that’s not useful, because instead of writting “–fontSize-12” you can just type in “12px”, right?
Why not using the same numbers as for the color themes.
400 for normal sized font
500 .. 900 for steps to bigger sizes, ideally in 100s steps and if needed 50s or even 10s steps between
300 ..100 for smaller font sizes
I would do something like this:
I had similar dilemma with colors, and first I defined basic brand color variables which later are assigned to context based variables:
For example:
We use names of cities sorted by population for all font sizes. They are easy to expand if you take a country like US – if you need a new font size between 12px and 15px, just pick a city with population in between. And they are not related to UI, hence flexible to reuse across different components.
The “T-Shirt” system.
Not sure what architecture/ technique this is, but in my current job we stick to the following style:
$color-blue
$color-white
$font-size-body
$font-size-body-small
$font-size-body-smaller
$spacer-small
$spacer-medium
$spacer-large
Find it very pragmatick and easy to follow.
I’ve been jumping on the tailwind name conventions, and use xxs, xs, s, m, l, xl,xxl and so on.
Well, I usually keep it simple and use “rem” or “em”. Therefore I define a common font size (in general 18px) and every other element is defined by that (for example 2.0rem).
The advantage is, that I can easily change the main font size and every other element is changed accordingly.
Therefore, I don’t use variables for font sizes.
We usually do it as a scale, with a base unit. A lot like font weights.
For example, if our base unit for
body
text is16px
, the font size would be$size-fnt--100
. Then we create the scale from there as needed, but try to avoid adding new ones unless 100% necessary. So:$size-fnt-body--100
is16px
. We could set this as$size-fnt-body
to create a default return.$size-fnt-body--150
is16px * 1.5
or24px
$size-fnt-body--75
is16px * .75
or12px
$size-fnt-body--200
is16px * 2
or32px
And we’d follow the same pattern for any other new font
types
. Likeheader
base unit is22px
:$size-fnt-header--100
is22px
. We could set this as$size-fnt-header
to create a default return.$size-fnt-header--150
is22px * 1.5
or33px
$size-fnt-header--75
is22px * .75
or16.5px
$size-fnt-header--200
is22px * 2
or44px
Or maybe we want to introduce a
caption
size with a base of10px
…$size-fnt-caption--100
is10px
. We could set this as$size-fnt-caption
to create a default return.$size-fnt-caption--150
is10px * 1.5
or15px
$size-fnt-caption--75
is10px * .75
or7.5px
$size-fnt-caption--200
is10px * 2
or20px
Pair this with CSS variables for more control responsively or to scope your variables…
Start out with adding
--size-fnt-body: 16px
to whatever scope you’re in.Start out with adding
--size-fnt-body: 18px
to a new media query (let’s say>1280px
).Start out with adding
--size-fnt-body: 14px
to some modifying class to update the scale (let’s call this.--some-modified-class
)$size-fnt-body--100
isvar(--size-fnt-body)
. We could set this as$size-fnt-body
to create a default return.@>1280px
return:18px
.--some-modified-class
return:14px
Other resolutions
return:16px
$size-fnt-body--150
isvar(--size-fnt-body) * 1.5
@>1280px
return:27px
.--some-modified-class
return:21px
Other resolutions
return:16px
$size-fnt-body--75
isvar(--size-fnt-body) * .75
@>1280px
return:13.5px
.--some-modified-class
return:10.5px
Other resolutions
return:16px
$size-fnt-body--200
isvar(--size-fnt-body) * 2
@>1280px
return:32px
.--some-modified-class
return:28px
Other resolutions
return:16px
Add or remove to the scale when only absolutely necessary…
$size-fnt-body--100
is16px
. We could set this as$size-fnt-body
to create a default return.➡️➡️➡️
$size-fnt-body--125
is16px * 1.25
or20px
$size-fnt-body--150
is16px * 1.5
or24px
$size-fnt-body--75
is16px * .75
or12px
$size-fnt-body--200
is16px * 2
or32px
We use this same methodology across colors, spacing, elevation, font sizes, and also paragraph widths. It’s pretty killer when paired with Intellisense and you know how to ‘query’ the variables, which is a small learning curve to get the nomenclature. Once solidified it’s quite scalable.
$size-{{ namespace }}-{{ type }}--{{ scale }}
: Size variables$size-fnt-{{ type }}--{{ scale }}
or$size-fnt-header--75
$size-spacing-{{ type }}--{{ scale }}
or$size-spacing-ui--100
$size-p-{{ type }}--{{ scale }}
or$size-p-width--75
$clr-{{ namespace | type }}--{{ category }}--{{ type | scale }}--{{ modifier }}
: Color variables (semantic or not)Source colors
$clr-banana--{{ scale }}--{{ modifier }}
or$clr-banana--100
or$clr-banana--100--hover
Semantic colors
$clr-ui-surface--{{ category }}--{{ scale }}--{{ modifier }}
or$clr-ui-surface--bg--100
or$clr-ui-surface--bg--100--hover
I do like this one indeed! The variable name declared the scale ratio, and you can easily add as many as you want in the future without breaking it.
I start with a foundation of variables that will map to heading sizes needed
fontSize100 => h1
fontSize200 => h2
fontSize300 => h3
etc.
and then fill in with other needed sizes required by the design in the correct slot / scale
fontSize50 > h1
h1 > fontSize150 > h2
h3 > fontSize320 > h4
h3 > fontSize340 > h4
h3 > fontSize360 > h4
Plus, other helper variables, which are sometimes merely assigned one of the previous numerical fontSize variables. Nothing wrong with some redundancy if it saves a little mental energy.
fontSizeText
fontSizePullQuote
fontSizeCaption
This is what I do, I think the number scale makes sense and it matches other types of content.
/* display */
–display-1: 80px;
–display-2: 72px;
–display-3: 64px;
–display-4: 56px;
–display-5: 48px;
–display-6: 40px;
I previously used names like
title
,subheadline
andbody
for font size variables. The issue I’ve found is that when working on different designs, you may want to use larger or smaller font sizes and then you semantics break down. So your your solution ends up with countless variables, some with identical values.Semantic naming makes more sense if you have a combination of different properties such as font size, font family, line height, letter spacing and so on.
For font sizes, I’ve moved to a linear scale which provides enough information about the sizing with respect to other sizes:
And use classes to describe semantic usage of typography: