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

Open
wants to merge 1 commit into
base: next
Choose a base branch
from

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 👍

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