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

Complete Support for Font relative length units: rem, em, ex, ch, cap, ic + more #47

Open
WebMechanic opened this issue Aug 27, 2022 · 9 comments
Labels
enhancement New feature or request

Comments

@WebMechanic
Copy link

WebMechanic commented Aug 27, 2022

With 0.2.0 alpha of the polyfill now supporting the poster childs rem and em for responsive designs, it would be nice to have complete support for font size relative length units to complete the picture.

With the ability to use the element related em unit for container widths, adding other font size relative units should be just a matter of "while-listing" and performing the same logic as for rem (root element font-size) or em (selected element font-size) respectively.

These are also long supported by browsers. For the selected element (like em):

  • ex: Equal to the used x-height
  • ch: Equal to the used advance measure of the “0” (ZERO, U+0030) glyph (Latin fonts), see ic below
  • cap: Equal to the used cap-height (capital Latin letter)

Browser support for additional font size relative sizes of CSS Values 4 is unknown / unavailable on caniuse or MDN, but could be added preemptively to be available when support is eventually implemented. Rules are virtually the same.

Selected element font:

  • ic: Equal to the used advance measure of the CJK water ideograph (CJK fonts), see ch above

Root element font:

  • rex
  • rch
  • rcap
  • ric

https://drafts.csswg.org/css-values-4/#font-relative-lengths
https://drafts.csswg.org/css-values/#relative-lengths
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Values_and_Units#dimensions
https://developer.mozilla.org/en-US/docs/Web/CSS/length

Thank you!


Some may argue that Point pt and Pica pc are also "font related". While certainly standard in print and PostScript, both are actually absolute units based on the inch. Given their straight forward computation into pixels, both could be easy to implement, despite being highly inaccurate for modern screen resolutions. However, designers coming from print might find them useful.

  • pt: 1/72nd of 1in.
  • pc: 1/6th of 1in == 12pt.
@WebMechanic WebMechanic changed the title Complete Support for Font relative length units: rem, em, ex, ch, cap, ic + more (CSS Values 4) Complete Support for Font relative length units: rem, em, ex, ch, cap, ic + more Aug 27, 2022
@devknoll devknoll added the enhancement New feature or request label Aug 27, 2022
@devknoll
Copy link
Collaborator

Thanks for your suggestion!

The way that em and rem support work today is that the polyfill will read the computed font-size property on the appropriate elements to get the canonical px scalar value that can be used for computation and comparison.

At the moment, it's not clear to me how the additional units could be supported in a cross-browser way.

Ideally, we'd just be able to set a custom property to e.g. 1ex and read it back to get the value. Unfortunately, that won't work, as the computed value of a custom property is the input.

Another thought would be to just create children elements that we can use to measure. Unfortunately, without using ShadowDOM, this would be quite brittle. And that has its own issues with e.g. elements that you can't attach a ShadowDOM to, elements that already have a ShadowDOM attached to them, etc...

The last thing we might be able to do is find all of properties that could change the value of e.g. ex and use that to either mirror to another hidden element on the page or possibly drive a canvas that we can use TextMetrics on (though I'm unsure if the latter would work). For this case, we'd definitely only want to do this for elements that you're actually using those units in.

Would love to hear any other ideas!

@WebMechanic
Copy link
Author

Unfortunately, without using ShadowDOM, this would be quite brittle

not sure about the actual "brittleness" or if that's hypothetical, but in a project also in need to translate anything to pixels I used the following quiet successfully.

/**
 * @param {HTMLElement} scope  the container element
 * @param {Number}      size
 * @param {String}      [unit='em']  any (font based) CSS unit.
 */
const toPixels = memoize((scope, size, unit = 'em') => {
  const test = document.createElement("div");
  test.style.position = 'absolute';
  test.style.fontSize = `1${unit}`;
  test.style.width = `1${unit}`;
  scope.appendChild(test);
  const val = test.offsetWidth;
  scope.removeChild(test);
  return val * em;
});

Absolute positioning is required to prevent the test element from being "squished" by a sibling.

const elt = document.querySelector(`.container`);
elt.style.fontSize = '20px'; // or whatever the CSS said
const em  = toPixels(elt, 1);        // 20
const px  = toPixels(elt, 10, 'px');  // 10
const ex  = toPixels(elt, 1, 'ex');  // 6
const ch  = toPixels(elt, 1, 'ch');  // 8
const cap = toPixels(elt, 1, 'cap'); // 11

Results will likely vary and depend on the font family used as expected.

While this is untested with a gazillion container elements using all different widths to be measured, in "real life" this wasn't a bottleneck nor "brittle". Any modern frontend framework like React or Vue will do worse things while fiddling with the DOM.

The result of this snippet is virtually instant. Even if scope referred to an element with display flex or grid, there was no (noticeable) layout shift when the temporary test element was added.
However, I can't tell how this might perform in a non-throttled, non-debouncing ResizeObserver callback.

Thanks.

@devknoll
Copy link
Collaborator

devknoll commented Aug 27, 2022

I think something like that certainly would work in a lot of cases. My concern about brittleness though is mostly that it's really easy to accidentally break in real world code. You just need a rule like...

.container * {
  font-weight: lighter;
}

And there's a chance that the measurements will be subtly wrong. The polyfill could try to detect this, but then you might as well just copy the relevant styles from .container to a different element anyway.

Personally, I think I would lean toward the ShadowDOM approach. It's already expected that the polyfill should be loading early enough to hook element.attachShadow reliably, and it seems like it's probably reasonable to restrict the full set of font relative units to elements that can host a shadow tree. That subset of elements isn't terribly draconian, but does exclude things like <ul>/<ol> which is unfortunate.

It has a bonus that we can actually keep the dummy measurement element around too and have the browser tell us when something changed the value (via ResizeObserver).

Edit: Corrected font-weight value

@WebMechanic
Copy link
Author

font-weight: thin does not exist (nor does light), and an internal font metric like ch won't change with font-weight. It should for font-stretch if it's a variable font and that would actually be the expected behaviour.... in any case it's precisely the size I want the content to live in...
...but I get your (hypothetical) point.

does exclude things like <ul>/<ol> which is unfortunate.

well... that's a huge understatement as it would be a total show-stopper for us and presumably anyone who intends to ship semantic, accessible code.
We certainly wouldn't want to start to create a meaningless div soup with tons of aria attributes and additional JavaScript only to "simulate" what browsers already do way better and much faster.

I understand that developers love predictability and this mix of font relative units don't provide that. For some they're counter intuitive.
The ch or ex in Roboto gives different values than in Times Roman - "what a mess", and yet that's the beauty of it 'cos relative font units always line up nicely with each other whatever an element's font family, it's size, weight or width would be, and when used in conjunction more robust, and equally important: very pleasant to look at.
From a designer's perspective they're more powerful than pixel and even rem values only.


The moment content design is based on font relative units, pixel perfection is irrelevant: every other font family on any system would change the final pixel result anyways, as could properties like text-transform or font-stretch.
Different browsers will even render the same font different on the same system, and that's irrelevant too.

The point is: a size of 20ch will be consistent for any specific font setting in each browser on the very page for the element it's used.
There's a reason I want something to be compared to 20ch - not 20ex or 10em or 160px.
I don't care for some pixel value, I care for 20ch in a specific context.

We eventually had very little use for the 0.1.2 release because of its limitation to pixels.
I have yet to test how far we may get with the new addition of rem and em.

Our designs rely on these various internal font metric values and we would have to add "magic numbers" when converting them to rem/em in the hopes to get close to the same natural scales and harmonies we get by using proper and robust ch or ex.
If the font family changes for whatever reason (and it does on each system), we can't rely on magic numbers for layout. It may not break, but parts of it would crumble.

@devknoll
Copy link
Collaborator

devknoll commented Aug 29, 2022

font-weight: thin does not exist (nor does light)

Oops, I meant lighter. Thanks for the correction.

and an internal font metric like ch won't change with font-weight

No -- but font-weight can change which font is resolved, which can have a different ch value. Here's an example.

that's a huge understatement as it would be a total show-stopper for us

Let me spend some more time thinking and looking into the alternatives and get back to you.

Thanks!

@WebMechanic
Copy link
Author

WebMechanic commented Aug 31, 2022

but font-weight can change which font is resolved, which can have a different ch value.

of course it can, which is the whole point of using this unit in the first place :D

That's also part of responsive design: respond to the various fonts available on the target system and only then will a ch be a ch (extended Codepen).

For the same reasons it's often a bad idea to mix pixels and percentages1, it's a bad idea to mix certain font metrics.
ex, em and cap result in perfectly "stable" numbers per unit 'cos they are based on the square/heights of the glyphs, which virtually never change due to the font style within a font family.
"ch" and "ic" can vary, however, as they represent glyph widths that may change depending on the font style or variant -- as seen in your Roboto example (thank you for that btw!)

Just because it all ends up with pixels at the end of the food chain does not mean that designs (should) depend on pixel values or any other fixed unit, and luckily in 2022 they no longer have to.
If they do: use pixels.
If one wants "10 characters" based off the active font in the given context they'll use ch.

Simply drop the idea that layouts only work with "fixed" units. Leave it to the (UI) designer what unit is best for a given container. Hopefully designers know what they're doing...
It's not up to a tool to tell what units produce the right or wrong design result.

thanks 4 ur ⏲, much appreciated

Enjoy!

Footnotes

  1. a problem Bootstrap eventually "solved" and what made it so successful, sadly also unifying all website designs

@WebMechanic
Copy link
Author

I believe @argyleink may have valuable insight working on VisBug and he knows a thing or two about CSS

@argyleink
Copy link
Member

From what I've learned while waiting for the lh unit and other font relative features, is that the font information isn't always even available for the browser engine, let alone JS. Fonts themselves can have bad information in them that's been accounted for by a browser, like impossible stuff for a polyfill author to know about font measurements. I agree attempts would work in many cases but not all (hence the brittle comment) because all JS can do is synthetically attempt a recreation of the environment needed to make a measurement, and while it'll be fine for many fonts, it wont be perfect for all of them.

But, to devils advocate my own comment, that's all a polyfill can do, is synthetically attempt as best it can, so maybe that's not a good enough reason to reject the feature. Definitely up to the authors of the plugin here, to let them assess their own amount of fidelity and complexity they want to balance and manage over time with this polyfill.

So I see both sides here! Sorry if that's not helpful lol. But it's a fair request to want to use relative fonts in a container query, the spec allows it and the final implementations will too, but it's also on a fine line cusp for maintenance and management from a maintainer POV, and few folks may actually leverage this feature. ⚖️

hope this was helpful 🙂

@WebMechanic
Copy link
Author

Thank you, Adam, for the input!

Fonts themselves can have bad information in them that's been accounted for by a browser, like impossible stuff for a polyfill author to know about font measurements.

I don't want the Polyfill to query font internals or to include a mini wakamaifondue and we're not (yet) dealing with line blocks and the trouble this could bring (hence no lh and easy vertical rhythm control in browsers either... yet).

There sure are tons of shitty fonts out there and "garbage in, garbage out", but that's also my point: no matter how shabby the font might be, the browser figured out "something" and uses defined fallback calculations, like 0.5em for 1ex if the x-height isn't properly encoded in the font.

In any case, it's the designer's job and responsibility to choose a proper font, for the same reason s/he decided to go for a 20ch or 36ex container width.

I'm sure people who understand Typography are aware of the issues bad and broken fonts can have (on the web and in design apps), and therefore use quality web fonts and a good fallback stack.
Everybody else will use pixels and divs.

So even garbage font results will be consistent for that font on that page and produce something very close to the intended design. It will of course be different with a nice perfectly font.

With great power bla bla bla ...
We can only make suggestions to the browsers anyway.

Thanks.


We are in the middle of a complete redesign, so unfortunately I have little time for elaborate tests, but I eventually cloned the repo to play with the original code.

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

No branches or pull requests

3 participants