• Font size: An Unexpectedly Complex CSS Property
  • Manish Goregaokar
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: zephyrJS
  • Proofread by: Bambooom, Colafornia

Font size is a bad CSS property

This is probably a property that everyone who has written CSS knows about. It’s everywhere.

But it’s also very complicated.

“It’s just a number,” you say. “How complicated can it be?”

I thought so, too, until I started working on stylo.

Stylo is a project to integrate Servo’s style system into Firefox. This style system is responsible for parsing CSS, determining which rules apply to which elements, cascading the rules through them, and ultimately calculating and assigning styles to the elements in the tree. This happens not only during page loading, but also when various events (including DOM operations) are triggered, and is an important part of page loading and interaction time.

Servo uses Rust and uses its parallel security features in a number of ways, style being one of them. Stylo has the potential to bring these acceleration techniques to Firefox, as well as the security of code that comes with a more secure system language.

Anyway, in terms of a style system, I think font size is the most complex property it has to deal with. Some properties can be more complex when it comes to layout or rendering, but font size is probably the most complex property in styles.

I hope this article gives an idea of how complex the Web can become, and also serves as a document on some complex issues. In this article, I’ll also try to explain how a style system works.

good Let’s see how complicated font size is.

basis

The syntax for this property is very simple. You can specify it as:

  • The length value (12px.15pt.13em.4in.8rem)
  • Percentage value (50%)
  • Put the above together and use CALC to calculate (calc(12px + 4em + 20%))
  • Absolute keywords (medium.small.large.x-large, etc.).
  • Relative keywords (larger.smaller)

The first three usages are common with length-dependent CSS properties. Syntax has no exceptions.

The next two are interesting. In essence, absolute keywords map to various pixel values and match the result (size=3 is equivalent to font size: medium, for example). The actual values they map to are not simple, as I’ll discuss in a later article.

Relative keywords are basically scaled up or down. The mechanics of scaling are also complex, but that has changed. And I’ll talk about that as well.

Em and REM units

First: EM units. You can specify a value in em or REM in any length-based CSS property.

5em means “5 times the font size applied to the element”. 5REM means “5 times the font size of the root element”

This means that the font size needs to be calculated before all other attributes (well, not exactly, but we’ll talk about that!). So that it is available during that time.

You can also use em units in font size. In this case, it is evaluated relative to the font size of the parent element rather than its own font size.

Minimum font size

Browsers allow you to set a “minimum” font size in their preferences, and text cannot be smaller than this font size. This is helpful for people who have difficulty reading small print.

However, this does not affect the em font size attribute.

There will be a small height (you can notice by color), but the text size will be limited to the minimum size.

In practice this means that you need to keep track of two separate computed font size values. One of these values is used to determine the font size of the actual text (for example, to calculate EM units). A different value is used when the style system needs to know the font size.

But it gets more complicated when it comes to Ruby (marginal markup). In ideograms (usually kanji and Kanji based Japanese and Korean characters), it is sometimes useful to express the pronunciation of each character in pinyin characters to help readers who are not familiar with kanji, which is known as “Ruby” (called “zhenuri 仮 name” in Japanese). Because these words are ideographic, it is not uncommon for learners to know the sound of a word but not how to write it. For example, it is necessary to display Japan hijo ほ graphic, in Japanese (read as “nihon” in Japanese) with Ruby to add hiragana hijo ほ GRAPHIC.

As you can see, ruby text in the pinyin section has a smaller font (usually 50% of the font size of the main text). Minimum font size complies with this and ensures that if Ruby applies 50% font size, ruby’s minimum font size is 50% of the original minimum font size. This avoids the Japanese hippo ほ graphic (up and down two words set into the same size hours), so it looks very ugly.

Word gets bigger

Firefox allows you to zoom text when zooming only. If you’re having trouble reading some small print, it’s nice to be able to zoom in on the text on the page without having to zoom in on the whole page (which means you need to scroll a lot).

In this example, other properties with EM units set are also enlarged. After all, they should be relative to the font size of the text (and may have some relationship to the text), so if that size has changed, they should change with it.

(Of course, this argument also applies to minimum font sizes. But I don’t know why the smallest font isn’t applied.)

It’s actually pretty easy to do. When calculating absolute font sizes (including keywords), they are scaled if text scaling is enabled. The rest is business as usual.

The < SVG :text> element disables text scaling, which also raises some pretty tricky questions.

Episode: How does the style system work

Before moving on, I need to give you an overview of how the style system works.

The style system’s job is to take the CSS code and DOM tree and assign computed styles to each element.

“Specified” and “computed” are different here. Specified styles are styles specified in THE CSS, and computed styles are those that are attached to the element, sent to the layout, and inherited from the element. The specified style can be computed to different values when applied to different elements.

So when you specify width: 5em, it might calculate width: 80px. The calculated value is usually the result of cleaning up the specified value.

The style system parses the CSS first, usually generating a set of rules that contain declarations (declarations like width: 20%; ; Property name and specified value)

It then traverses the tree in top-down order (which is parallel in Stylo) to find which declarations apply to each element and the order in which they are executed – some declarations take precedence over others. It then evaluates each related declaration based on the element’s style (parent style and other information) and stores that value in the element’s “computed style.”

To avoid duplication of effort, Gecko and Servo did a lot of optimization here 2. A Bloom filter is available to quickly check whether the deep descendant selector is applied to the subtree. There is a “rule tree” for caching the identified declarations. Computed styles are often referenced, counted, and shared (because the default state is inherited from the parent or default style).

In general, this is how style systems work.

Key value

Well, this is where things get complicated.

Remember when I said font-size: medium would map to a value?

So what does it map to?

Well, it turns out, it depends on the font. For the following HTML:

<span style="font: medium monospace">text</span>
<span style="font: medium sans-serif">text</span>
Copy the code

You can see the results from codepen.

text
text

The first font size is 13px and the second font size is 16px. You can get the answer from devTools’s Calculation Style window, or you can use getComputedStyle().

I think the reason behind this is that constant width fonts tend to be wider, and the default font size (medium) has been reduced to make them look similar widths, as well as all the other keyword font sizes have been changed. The end result is this:

Firefox and Servo have a matrix used to calculate the values of all absolute font size keywords based on “base size” (that is, font size: medium computed values). In fact, Firefox has three tables to support some legacy use cases, such as weird patterns (which Servo has not yet added support for). We query “Base size” in other parts of the browser based on language and font.

Wait, what does that have to do with language? How does language affect font size?

In fact, the base size depends on the font family and language, and you can configure it.

Firefox and Chrome (using extensions) actually allow you to set which fonts to use for each language, as well as the default (basic) font size.

It’s not as arcane as people think. For non-Latin languages, the default font is often ugly. I installed a separate font that shows nice tiencheng fonts

Similarly, some scripts are much more complicated than Latin. The default font I set for Tiencheng text is 18 instead of 16. I’ve already started learning Mandarin, and I’ve set the font size to 18. Chinese glyphs can get quite complicated and I still have a hard time learning (and recognizing) them. Larger fonts are more helpful for learning them.

Anyway, it doesn’t make things too complicated. This does mean that font family needs to be evaluated before font size, and font size needs to be evaluated before most other attributes. The language can be set through the LANG property of HTML, which, because it is inheritable, is internally treated as a CSS property that must be computed as soon as possible.

So far, so good.

Now, the unexpected is happening. This dependency on language and family is inheritable.

Look, what’s the font size inside div?

<div style="font-size: medium; font-family: sans-serif;"> <! -- base size 16 --> font size is 16px <div style="font-family: monospace"> <! -- base size 13 --> font size is ?? </div> </div>Copy the code

For inheritable CSS property 3, if the parent evaluates to 16px and the child element has no other value specified, the child element inherits the 16px value. The child element does not care where the parent element got the computed value from.

Font size now “inherits” a value of 13px. You can see the results from codepen here:

font size is 16px

font size is ??

Basically, if the calculated value comes from the keyword, then no matter how font family or language changes, font size will be recalculated using the font family and language in the keyword.

The reason for this is that the different font sizes won’t work otherwise. The default font size is medium, so the root element will basically get a font size: medium and other elements will inherit this declaration. If you change it to a monospaced font in your document or use another language, you need to recalculate the font size.

Not only that. It even passes relative unit inheritance (IE excepted).

<div style="font-size: medium; font-family: sans-serif;"> <! -- base size 16 --> font size is 16px <div style="The font - size: 0.9 em"> <! -- Could also be font size: 50%--> font size is 14.4px (16 * 0.9) <div style="font-family: monospace"> <! -- Base size 13 --> font size is 11.7px! </div> </div>Copy the code

(codepen)

<div style="border: 1px solid black; display: inline-block; padding: 15px;">
    <div style="font-size: medium; font-family: sans-serif;">font size is 16px
        <div style="The font - size: 0.9 em"Word-wrap: break-word! Important; "> <div style=" text-align: center"font-family: monospace"> the font size is 11.7 px! </div> </div> </div>Copy the code

Therefore, when we inherit from the second div, we actually inherit 0.9*medium instead of 14.4px.

Another way of looking at this is that whenever font family or language changes, you should recalculate the font size as if language and family had not changed.

Firefox uses both strategies. The original Gecko style system dealt with this by actually returning to the top of the tree and recalculating the font size, as if language and family were different. I suspect this is inefficient, but the rule tree seems to make it slightly more efficient.

On the other hand, while computing, Servo stores some extra data, which is copied into child elements. Basically, you store something like: “Yes, this font is computed from the keyword. The key word was medium, and we applied factor 0.9 to it.” 4

In both cases, this leads to increased complexity for all other font sizes, as they need to be carefully protected in this way.

In Servo, most situations are handled by font size custom cascade functions.

Larger/smaller

I mentioned earlier that font: larger/smaller is scaled, but I didn’t mention the corresponding scale value.

According to the specification, if the current font size matches the value of the absolute keyword size (medium, Large, etc.), the value of the previous or the next keyword size should be selected.

If it is between two absolute keyword values, look for points of equal proportion between the first or last two sizes.

Of course, this has to deal nicely with the weird inheritance of keyword font sizes mentioned earlier. This is not too difficult in a GECko model because GECko recalculates anyway. In Servo’s modules, we store a series of Larger /smaller applications and relative units, rather than just one relative unit.

Also, when this value is evaluated during text scaling, you must first unscale, then look it up in the table, and then rescale it.

Overall, a lot of complexity didn’t bring much profit — it turned out that only Gecko actually followed the spec! Other browser engines use simple scaling.

So my solution was to remove this behavior from Gecko. Simplifies the process.

MathML

Firefox and Safari support MathML, the mathematical markup language. It’s not used much on the web these days, but it does exist.

MathML has its complications when it comes to font sizes. Especially scriptminsize, scriptlevel and scriptsize Emultiplier.

For example, in MathML, the numerator, denominator, or superscript is 0.71 times the font size of the external text. This is because the default scriptsizemultiplier for MathML elements is 0.71, and the default scriptlevel for these specific elements is +1.

Basically, scriptlevel=+1 means “font size times scriptsize emultiplier”, and scriptlevel=-1 is used to eliminate this effect. This can be specified by setting the ScriptLevel attribute on the mstyle element. You can also adjust the (inherited) multiplier by scriptsizemultiplier and the minimum by scriptminSize.

Such as:

<math><msup>
    <mi>text</mi>
    <mn>small superscript</mn>
</msup></math><br>
<math>
    text
    <mstyle scriptlevel=+1>
        small
        <mstyle scriptlevel=+1>
            smaller
            <mstyle scriptlevel=-1>
                small again
            </mstyle>
        </mstyle>
    </mstyle>
</math>
Copy the code

It looks like this (Firefox is required to view the rendered version, and Safari also supports MathML, but not very well) :

textsmall superscript text small smaller small again

(codepen)

So it’s not so bad. It’s as if scriptlevel is a weird EM unit. It’s no big deal. We already know how to deal with these problems.

And scriptminsize. This allows you to set the minimum font size for changes caused by scriptlevel.

This means that scriptminSize will ensure that scriptlevel does not result in fonts smaller than the minimum size, but it will ignore em units and pixel values that are specifically specified.

A bit of subtle complexity has been introduced here, and now scriptlevel is another factor affecting how font size inherits. Fortunately, within Firefox/Servo, scriptlevel (along with scriptminsize and scriptSizemultiplier) is also handled as a CSS property, This means we can use the same framework as font family and language — script properties are evaluated before font size is set, and if scriptlevel is set, font size recalculation is forced, even if the font size itself is not set.

Episodes: Early and late processing properties

In Servo, the way we handle attribute dependencies is by having a set of “early” attributes and a set of “late” attributes that allow dependencies on early attributes. We did two lookups of the declaration, one for an earlier attribute and one for a later attribute. Now, however, we have a fairly complex set of dependencies where font size must be computed after language, font-family, and script properties, but before everything else involving length. Also, because of another font complexity I didn’t talk about, font-family must be evaluated after all the other early attributes.

The way we deal with this problem is to separate font size and font family from the early calculation and then deal with it after the early calculation is complete.

In this phase, we first deal with disabling text zooming, and then with the complexity of font family.

Then calculate the font family. If the font size is specified, the calculation is performed. If font family, lang, or scriptlevel is specified but not specified, the calculation is forced as inheritance to handle all constraints.

Why did ScriptminSize become so complicated

Unlike other “minimum font sizes”, using em units in any attribute when the font size is limited by scriptminsize will use a clamp value to calculate the length, rather than an “if not clamped” value if the font size is limited by scriptminsize. So, at first glance, dealing with this seems simple; When scaling is required because of scriptlevel, only the minimum font size scriptminsize is considered.

As usual, things are not that simple 😀 :

<math>
<mstyle scriptminsize="10px" scriptsizemultiplier="0.75" style="font-size:20px">
    20px
    <mstyle scriptlevel="+ 1">
        15px
        <mstyle scriptlevel="+ 1"> 11.25 px < mstyle scriptlevel ="+ 1""> Would be 8.4375, but is clamped at 10px <mstyle scriptlevel="+ 1""> Would be 6.328125, but is clamped at 10px <mstyle scriptlevel="1">
                                    This is not 10px/0.75=13.3, rather it is still clamped at 10px
                                        <mstyle scriptlevel="1">
                                            This is not 10px/0.75=13.3, rather it is still clamped at 10px
                                            <mstyle scriptlevel="1""> This is 11.25px again <mstyle scriptlevel="1">
                                                        This is 15px again
                                                    </mstyle>
                                            </mstyle>
                                        </mstyle>
                                </mstyle>
                        </mstyle>
                </mstyle>
        </mstyle>
    </mstyle>
</mstyle>
</math>
Copy the code

(codepen)

Basically, if you add layers multiple times after reaching the minimum font size and then subtract one layer, you will not immediately calculate the min size/multiplier value. This makes it asymmetric; if the multiplier factor does not change, a net +5 should have the same font size as an element with a net + 6-1.

So, what happens is that script level is calculated based on font size as if scriptminSize has never been applied, and we only use it if the script size is greater than the minimum size.

This not only tracks script levels but also multipliers changes. Therefore, this will eventually create another font size value to inherit.

To summarize, we now have four different concepts of inherited font size:

  • The main font size used by the style
  • The “actual” font size is the primary font size, but is limited to the minimum
  • (only in Servo) “keyword” size; That is, the size stored as a keyword and ratio if it is derived from the keyword
  • “Out-of-script” dimensions; It’s like scriptminsize never existed.

Another complication is that the following should still work:

<math>
<mstyle scriptminsize="10px" scriptsizemultiplier="0.75" style="font-size: 5px">
    5px
    <mstyle scriptlevel="1">
        6.666px
    </mstyle>
</mstyle>
</math>
Copy the code

(codepen)

If it is already smaller than scriptminsize, reducing the script level (to increase the font size) should not be suppressed, as it will later make it look too large.

This basically means that scriptminsize can only be used if the value corresponding to script level is greater than the minimum font size of the script.

In Servo, all of the MathML processing was perfectly solved by this wonderful function with more comments than code and some code around it.


That’s what you need to know. Font size is actually quite complex. Many web platforms hide such complications, but they can be fun to encounter.

(When I have to implement them, maybe not so much fun. 😂)

Thanks to Mystor, Mgattozzi, Bstrie, and Projektir for reviewing the draft of this article.


  1. Interestingly, in Firefox, this number is 50% for all Ruby, except when the language is Taiwanese Chinese (which is 30%). This is because Taiwan uses a pinyin script called Bopomofo, in which each Chinese character can be represented by up to three Bopomofo characters. Therefore, it is possible to choose a reasonable minimum size so that Ruby never goes beyond the text below. On the other hand, pinyin can be up to 6 letters, while Hiragana can be up to (I think) 5, and the corresponding “no overflow” will make the text look too small. Therefore, placing them on the font is not a problem, and instead we choose to use a larger font size for better readability. Also, Bopomofo Ruby is usually placed next to text rather than at the top, so 30% is better. (h/ T @upsuper pointed this out) ↩
  2. Other browser engines have other optimizations, but I’m not aware of them yet. ↩
  3. Some attributes are inherited and some are reset. For example,font-familyInherited — unless otherwise set. buttransformNo, if you apply a transform to an element and its children don’t inherit that property.↩
  4. This can’t be handledcalcS, this is a problem I need to solve. In addition to the ratio, an absolute offset is stored.↩

The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.