Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to line height functions #14335

Closed

Conversation

MartijnCuppens
Copy link
Contributor

@MartijnCuppens MartijnCuppens commented Sep 4, 2024

Introduction

In this PR, I’ll tackle 6 issues we currently have with the way line heights are used in Tailwind. Some of these issues have workarounds that work well, but the idea is that this PR uses a technique to avoid these workarounds. After I explain the issues, I’ll explain step-by-step how I ended up with the current implementation which might be a little overwhelming at first. The reason I combined these issues is because the solutions will continuously override each other.

Issues

1. Line heights are not inherited from parents

For example:

<div class="leading-thight">
  <div class="text-xl">
    `leading-thight` is ignored here.
  </div>
</div>

2. Line heights in mobile variants are reset

Details of this issue can be found here: #6504 and #2808.

3. The line height of text-lg feels a bit off

See #14223. The line height feels a bit “too loose”. This PR will make clear why I opened that PR 😃.

4. Hard to find sensible line heights when using arbitrary font-sizes

When using arbitrary values for font sizes, the default line height of 1.5 will be used (inherited from <body>). Let’s say we want to use text-[2.4rem], this will have a line height of 3.6rem, which is obviously too huge. We can fix this by using modifiers, but this somewhat makes the font-size utility the only utility that requires modifiers.

5. Named leadings sometimes are a little confusing

There is a difference between the normal line height (1.5) and the default line height (specified per font size in the config) which isn’t really clear (see https://x.com/firatciftci/status/1835705812133585062 for example). Also, in cases like text-3xl leading-tight, the leading class actually makes the line height less tight.

6. <small> influences the line height

See https://play.tailwindcss.com/BXck05lL3v. The line height is set via the text-2xl, and because <small> is aligned to the baseline, there will be more whitespace at the bottom.

The fixes

Inheritance with custom properties

To fix (1) and (2), we can rewrite the leading utilities like this:

.leading-tight {
  --tw-line-height: 1.25;
  line-height: var(--tw-line-height);
}

And the font size utility like this:

.text-2xl {
  font-size: 1.5rem;
  line-height: var(--tw-line-height, 2rem);
}

This way, the line height of a parent (or the element itself) with a leading will be used if present.

Edit: this will probably be fixed in #14403.

Finding a line height function

For the other issues we’re going to do something different. Let’s start by putting the font sizes and their line heights in a table. We’ll convert everything to rem, so it’s easier to understand:

  Font size (rem) Line height (rem)
text-xs 0.75 1
text-sm 0.875 1,25
text-base 1 1.5
text-lg 1.125 1.75
text-xl 1.25 1.75
text-2xl 1.5 2
text-3xl 1.875 2.25
text-4xl 2.25 2.5
text-5xl 3 3
text-6xl 3.75 3.75
text-7xl 4.5 4.5
text-8xl 6 6
text-9xl 8 8

Ideally, we would have some kind of relation between the font sizes and line heights, but because the line heights of tailwind are hand-picked, we do not have this. However, there seem to be 4 line height functions that partially have a relation with the current values.

Some notes before reading the next table:

  • I used linear equations to calculate the line heights. I'll spare you the details, because it’s all just boring mathematics.
  • The line height of text-lg is already adjusted in the following table (fixing issue 3)
  • Matching line heights are bold & italic
  Font size (rem) Line height (rem) A:
2em-.5rem
B:
1em + .5rem
C:
2em / 3 + 1rem
D:
1em
text-xs 0.75 1 1 1.25 1.5 0.75
text-sm 0.875 1.25 1.25 1.375 1.583333333 0.875
text-base 1 1.5 1.5 1.5 1.666666667 1
text-lg 1.125 1.625 1.75 1.625 1.75 1.125
text-xl 1.25 1.75 2 1.75 1.833333333 1.25
text-2xl 1.5 2 2.5 2 2 1.5
text-3xl 1.875 2.25 3.25 2.375 2.25 1.875
text-4xl 2.25 2.5 4 2.75 2.5 2.25
text-5xl 3 3 5.5 3.5 3 3
text-6xl 3.75 3.75 7 4.25 3.5 3.75
text-7xl 4.5 4.5 8.5 5 4 4.5
text-8xl 6 6 11.5 6.5 5 6
text-9xl 8 8 15.5 8.5 6.333333333 8

If we take a closer look, we’ll notice the line height should be the minimum of A,B&C, and at least D. Thanks to css functions, we can write this as:

line-height: max(1em, min(2em / 3 + 1rem, min(1em + .5rem, 2em - .5rem)));

Which means font size utilities can look like:

.text-2xl {
  font-size: 1.5rem;
  line-height: max(1em, min(2em / 3 + 1rem, min(1em + .5rem, 2em - .5rem)));
}

Using the line height function

Ok, now let’s have a look at what we can do with this. We can now drop all line heights defined in the config, because the line height function will take care of it. Now the line height of arbitrary values are automatically calculated based on the idea of decreasing the line height for larger font sizes. If we take our example from issue 4 (text-[2.4rem]), we’ll see that the calculated line height falls in the 2em / 3 + 1rem range, so the line height will be 2.6rem. Much better.

Keeping support for using line heights in the config

Our brand new line height function provides sensible defaults, but someone might want to use a fixed line-height instead. If we also want to support line height inheritance, we can rewrite our font size utilities like this if a line height is present in our config:

.text-2xl {
  font-size: 1.5rem;
  line-height: var(--tw-line-height, var(--font-size-2xl--line-height));
}

Fixing named line heights

Now let’s have a look at how to improve our named leadings. Using distributivity, we can isolate 1.5 in our line height function:

max(1em, min(2em / 3 + 1rem, min(1em + .5rem, 2em - .5rem)))

becomes:

max(1em, 1.5 * min(4em / 9 + 2rem / 3, min((2em + 1rem) / 3, (4em - 1rem) / 3)))

Now that we have isolated 1.5, we can change this number by the other line height factors, for example:

.leading-tight {
  --tw-line-height: max(1em, 1.25 * min(4em / 9 + 2rem / 3, min((2em + 1rem) / 3, (4em - 1rem) / 3)));
  line-height: var(--tw-line-height);
}

Let’s have a look at how this looks when we plot the calculated line heights with the font sizes. I’ve used the relative line heights and a logarithmic scale to better see what is going on:

Relative line height vs font size

The only difference between “default tailwind” (pre PR) and “adjusted normal” is the text-lg value. All other named leadings are now adjusted to the value of the font size.

Now that we have adjusted our line-height/leading config to variables, we can use the --line-height-normal variable in the font size utility:

.text-2xl {
  font-size: 1.5rem;
  line-height: var(--tw-line-height, var(--line-height-normal));
}

In the meanwhile, we have also fixed issue 5. The “default line height” is now equal to the normal line height, and named line heights have the expected influence on font sizes (tighter, more relaxed,...).

<small> fix

The last fix is just a small one, we just need to adjust the line height to fix the whitespace:

small {
  font-size: 80%;
  line-height: var(--tw-line-height, var(--line-height-normal));
}

Remarks

  • I'm using prefixed (--tw-line-height) and unprefixed --line-height-normal custom properties, because I assumed configurable variables aren't prefixed, but internal variables are.
  • The whole min(4em / 9 + 2rem / 3, min((2em + 1rem) / 3, (4em - 1rem) / 3)) function can be moved to a separate variable to make things a little more readable.
  • The tests shuffled the order of some utilities, not sure why. But that's why there are so many changes there.

@adamwathan
Copy link
Member

Hey @MartijnCuppens this is very interesting, really appreciate the effort on this one!

I love the idea of being able to get a sensible line-height automatically when setting a font-size, my only real hesitation is a gut feeling around things feeling sort of cryptic and magical — max(1em, 1.5 * min(4em / 9 + 2rem / 3, min((2em + 1rem) / 3, (4em - 1rem) / 3))) is just a very strange equation to the average user who doesn't understand the backstory here 🤔

The other thing I was wondering is are these functions always guaranteed to land on whole even numbers to avoid any chance weird display rounding issues? We deliberately made sure all of the default line-heights were even numbers so you could easily center text within elements sized using the spacing scale which is also all even numbers.

I also wonder if maybe even if we did use this approach if we should keep leading-tight, leading-loose, and similar using their existing values for backwards compatibility, and just change the default/global line-height? It seems like that might be enough that people almost never need to think about line-height in normal situations, but not sure.

Very intrigued either way but simultaneously cautious about making things overly complicated, haha. Let me know what you think 👍

@MartijnCuppens
Copy link
Contributor Author

MartijnCuppens commented Sep 20, 2024

Thanks for the reply!

I love the idea of being able to get a sensible line-height automatically when setting a font-size, my only real hesitation is a gut feeling around things feeling sort of cryptic and magical — max(1em, 1.5 * min(4em / 9 + 2rem / 3, min((2em + 1rem) / 3, (4em - 1rem) / 3))) is just a very strange equation to the average user who doesn't understand the backstory here 🤔

I know it's quite an overwhelming function at first. The main goal of this function is to help people with automatically providing a sensible default line height. Right now Tailwind forces you to set a line height. From a typographic perspective, this makes sense. But it doesn't if you want to just want to change the font sizes, and you're used that line heights are relative, like browser default.

Let's assume the following case which makes use of custom properties. If you for example use text-[length:var(--title-font-size)], you can just easily change --title-font-size, without the need of calculating and setting the line-height too.

With the solution in this PR, you will have the ability to use fixed line heights in your config and you will have a sensible default if you don't set them. The cryptic line height function will indeed be visible in code, but in return the font size config will be a lot cleaner and developers aren't required to set their line heights anymore.

The other thing I was wondering is are these functions always guaranteed to land on whole even numbers to avoid any chance weird display rounding issues? We deliberately made sure all of the default line-heights were even numbers so you could easily center text within elements sized using the spacing scale which is also all even numbers.

For the font-sizes Tailwind currently uses: yes, because they're just the same (except for text-lg, where I deliberatly changed the line height). Some other font sizes might indeed not be even and some aren't rounded, but you can set the line heights in your config if you're tweaking the font sizes to these values.

Here is a complete overview of the font sizes and line heights in px
Font-size in px Line height in px
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 10
10 12
11 14
12 16
13 18
14 20
15 22
16 24
17 25
18 26
19 27
20 28
21 29
22 30
23 31
24 32
25 32.66666667
26 33.33333333
27 34
28 34.66666667
29 35.33333333
30 36
31 36.66666667
32 37.33333333
33 38
34 38.66666667
35 39.33333333
36 40
37 40.66666667
38 41.33333333
39 42
40 42.66666667
41 43.33333333
42 44
43 44.66666667
44 45.33333333
45 46
46 46.66666667
47 47.33333333
48 48
49 49
50 50
51 51

If we really want to make sure the line heights are even for every font size that exist (includingvw font sizes), we can make use of the round() function to round the calculated line heights to 2px. This might influence browser support, I'm not sure what that currently is for v4.

I also wonder if maybe even if we did use this approach if we should keep leading-tight, leading-loose, and similar using their existing values for backwards compatibility, and just change the default/global line-height? It seems like that might be enough that people almost never need to think about line-height in normal situations, but not sure.

Good point, BC wise, this isn't the best idea. Since this is just config, I don't really mind removing this from the proposal.


I'll remove the change to the leading classes and rebase the PR now that #14403 is merged. Maybe I'll open a new PR and close this one, to make things a little clearer. I'll try to finish that by Monday Tuesday.

@MartijnCuppens
Copy link
Contributor Author

Closing this in favor of #14503.

The inheritance issue (1) here will be ignored for now since in #14223, inheritance was explicitly disabled in the custom property, probably because of possible breaking changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants