csswg-drafts icon indicating copy to clipboard operation
csswg-drafts copied to clipboard

[css-fonts-4] Feature for making text always fit the width of its parent

Open tobireif opened this issue 8 years ago • 35 comments

This thread shows that it's a widely required feature: https://twitter.com/sindresorhus/status/979363460411609091

Example of a workaround: Open https://tobireif.com/ and (eg using dev tools responsive mode) resize the page down to 250px width while watching the text "Welcome".

tobireif avatar Apr 11 '18 08:04 tobireif

This requires performing layout in a loop, which we generally have avoided. Requiring a round-trip through JS is valuable because authors are more likely to realize it has a large perf cost

litherum avatar Apr 11 '18 08:04 litherum

In the JS at https://tobireif.com/ I perform two passes - that's plenty for a good-enough result, and it doesn't impact perf in any noticeable way (the text-fitting is only done once in addition to the first main run). That could be a great option for browser implementers as well, and it shows that supporting such a CSS feature is very feasible.

tobireif avatar Apr 11 '18 08:04 tobireif

If this were a feature, I think it'd be best if it was a CSS function. (similar to calc or minmax)

Something like font-size: fit(8px, 48px); where 8px is the minimum font-size and 48px is the maximum font-size.

I think using a function, other than being useful for minimum and maximum sizes, relays the gravity of using the feature since surely it'll have some performance issues in extreme cases.

I'd love to see this in CSS!

timothyis avatar Apr 11 '18 11:04 timothyis

Great suggestions!

A lower limit and an upper limit both make sense.

Instead of font-size: fit(8px, 48px) it might be better to name it eg font-size: fit-width(8px, 48px).

tobireif avatar Apr 11 '18 11:04 tobireif

Changing font-stretch, especially using variable fonts, is another way to fit text into parent. Or compression during justification . So if there is a css property that instructs layout engine to fit, it should allow different methods and so likely be separate from font-size.

And this kind of functionality may not only be on line operation. It may be useful for more advanced functionality, like optimal paragraph layout, line clamping, or simple ellipsis.

SergeyMalkin avatar Apr 11 '18 17:04 SergeyMalkin

Changing font-stretch, especially using variable fonts, is another way to fit text into parent. Or compression during justification . So if there is a css property that instructs layout engine to fit, it should allow different methods and so likely be separate from font-size.

True! (also eg letter-spacing)

And this kind of functionality may not only be on line operation. It may be useful for more advanced functionality, like optimal paragraph layout, line clamping, or simple ellipsis.

Let's start simple 😀 If we're asking for too much we might not get anything. The basic simple use case of fitting one line of text (eg a heading) into its responsive parent is so common that a solution for that would cover a lot (and more could get added/specd later).

tobireif avatar Apr 11 '18 19:04 tobireif

Yes it's feasible to implement the functionality using JS, and yes there are workarounds, and I think there even is a lib, but it sure would be very handy to be able to simply write one single line of CSS instead.

My implementation in the source of https://tobireif.com/ is more than 50 lines of JS - if people could instead write a single line of CSS then that would save a lot of typing.

By the way @litherum : If the implementation is smart enough, perhaps one pass would be sufficient → no loop / double-pass.

Perhaps the syntax could look like this:

fit-width: font-size(20px, 100px);
fit-width: letter-spacing(-0.1em, 1.5em);
fit-width: any-text-width-affecting-property(min, max);

The sizing/fitting should honour the (potential) padding of the container.

tobireif avatar Apr 12 '18 07:04 tobireif

Using a small number of passes is unlikely to work in the general case, because if we get it wrong, the text will overflow its container and wrap, which would be catastrophic. Any generalized implementation would have to iterate until the algorithm converges. Such an algorithm would be a great way to make a browser hang.

litherum avatar Apr 14 '18 10:04 litherum

If you do want to provide this widely requested feature - perhaps you could try it out 😀 If your algorithm is smart regarding calculating the estimated target value, it will not need many passes, and it might need only one or two passes. For all and any cases.

Such an algorithm would be a great way to make a browser hang.

When you try it out, and limit the maximum number of passes to 2 == no browser hang at all, and if your algo can estimate the correct value pretty well, then there's a good chance that the result will be sufficiently good. You'd have to try it out though.

If you do not want to provide that feature no matter what, and thus do not want to create a quick "beta" implementation for seeing what's feasible, then there's not much reason to continue the discussion. In that case please close the ticket.

I did create a quick implementation using JS and found that it works sufficiently well using only two passes. The code is at https://tobireif.com/ -> source -> "var topLevelHeadings". It's just a quick (but good enough for that case) implementation - I'm 100% sure that you could come up with a much better (and generally applicable) algo 😀

Here's another JS implementation: http://fittextjs.com/ https://github.com/davatron5000/FitText.js

None of the above implementations causes any noticeable performance issue. And: The latter is a general lib.

tobireif avatar Apr 14 '18 11:04 tobireif

As for your own site, the type inside your headers is simple enough that you'd have performance gains in just using vw inside a breakpoint, reducing 50 lines of runtime js to possible 4 lines of css.

I'd prefer CSS that's based on the parent element width, not on the viewport width. (Because generally the element width might change without the viewport changing.)

The feature is a (very popular) wish - the specification of that feature (including all relevant details) would be up to the CSS WG.

tobireif avatar Apr 14 '18 16:04 tobireif

(Oh, and if that feature would be only feasible to spec/implement for a defined set of simple types of cases, I'm sure that simple feature would be widely appreciated as well 😀 The syntax still could be fit-width: any-text-width-affecting-property(min-value, max-value), I think.)

tobireif avatar Apr 14 '18 16:04 tobireif

If and when there will be an ew unit ("element width", as in EQCSS), and if and when there will be clamp() , then the functionality in this feature wish ticket here could be expressed sufficiently succinct, eg:

font-size: clamp(30ew, 20px, 80px);

tobireif avatar Apr 14 '18 17:04 tobireif

Where are you getting 30 in the 30ew? Are you matching that to the length of the string in the element? Or is 1ew just 1% of the elements width, meaning a 100px wide element would set its font-size to 30px?

jonjohnjohnson avatar Apr 14 '18 17:04 jonjohnjohnson

Here's the definition of ew, eh, emin, and emax from jsincss-element-units:

switch(unit) {

  case 'ew':
    return tag.offsetWidth / 100 * number + 'px'

  case 'eh':
    return tag.offsetHeight / 100 * number + 'px'

  case 'emin':
    return Math.min(tag.offsetWidth, tag.offsetHeight) / 100 * number + 'px'

  case 'emax':
    return Math.max(tag.offsetWidth, tag.offsetHeight) / 100 * number + 'px'

}

I was thinking of isolating just these tests into their own package (and maybe the element query tests from jsincss-element-query) so other plugin builders could more easily re-use the same tests in their plugins.

tomhodgins avatar Apr 14 '18 17:04 tomhodgins

Yeah, it'd not be the real deal where the implementation figures out the value required for making the text fit its container. It'd just be a pragmatic way to get the feature with just one line of CSS.

(And yes, 30ew means 30% of the element width. The exact number is just an example, it could be eg 45.5ew .)

tobireif avatar Apr 14 '18 17:04 tobireif

(I was replying to @jonjohnjohnson , just so there's no misunderstanding @tomhodgins 😀)

tobireif avatar Apr 14 '18 18:04 tobireif

Ideally we could state in CSS "always fit this word/line of text inside its parent (by auto-adjusting the property "foo" eg font-size or letter-spacing), no matter what font is used".

tobireif avatar Aug 22 '18 10:08 tobireif

We now do have clamp() to specify lower and upper bounds. Alas, we can only reference character width (ch, ic) or height (em, cap) and viewport dimensions (vw, vh etc.), not line or box width, as units. (Units for container dimensions have been proposed in #5888.) So you could only approximate the result for an assumed number of characters per line.

For the desired capability we would need new keywords or functions indeed.

Crissov avatar Feb 06 '21 10:02 Crissov

For the desired capability we would need new keywords or functions indeed.

Yep 😀

tobireif avatar Feb 08 '21 11:02 tobireif

No one has mentioned the SVG textLength property, which already does this. The functionality is also part of AH formatter: https://www.antenna.co.jp/AHF/help/en/ahf-ext.html#axf.overflow-condense

Their algorithm applies to a block, not a line - I expect that the text is progressively adjusted and layout retried until it fit. It's certainly going to be multi-pass and expensive - you can take a guess at a start value easily enough, but word-breaks at the end of the line necessarily make the algorithm iterative to find the best value. Doing it once for print layout is one thing, but it would be horrendous if you were resizing a window with this on.

We've been asked for similar functionality a few times over the years, but I believe only ever for "fit to line" rather than "fit to block". I think it's more of an issue in print, at least until they start selling paper with a horizontal scrollbar.

If you restricted it to just scaling either font size or font-stretch, and you restrict it to just scaling text to fit a single line, then it's theoretically a single pass - it's just a multiplier applied to the property. But it gets rapidly more complex when you've got only part of the line scaling this way, or you have multiple items on the line doing this with different layout properties - for instance, imagine a float and two spans with different initial font-sizes on the line, all trying to scale themselves to fill the line. It's all stuff that would need defining.

faceless2 avatar Feb 08 '21 12:02 faceless2

@faceless2 wrote:

If you restricted it to just scaling either font size or font-stretch, and you restrict it to just scaling text to fit a single line [inside a box]

That would be sensible (with font size as default).

tobireif avatar Feb 10 '21 10:02 tobireif

I’ve seen cases where this has been applied to each word, for some definition of word. Nonetheless, I guess it would be fine to do this by fitting the whole textual box content on a single line.

(An l or line element would have been better than br in HTML.)

Crissov avatar Feb 10 '21 21:02 Crissov

Hi all, I'd like to revive the conversation and provide another perspective on the utility of supporting a feature like this in CSS.

There are many designs that leverage careful placement and styling of text. A lot of time is spent by designers and engineers to implement these designs, but often only just in English. As soon as the text gets translated to another language, especially if the translation is much longer or shorter, applying the same CSS to the text that worked for English often causes issues such as text overflowing, truncating, breaking mid-word, widows, etc. As a result, this feature would make it easier to localize text while preserving design intent.

This requires performing layout in a loop, which we generally have avoided. Requiring a round-trip through JS is valuable because authors are more likely to realize it has a large perf cost

There are several JS libraries that attempt to implement this resizing. However, one limitation of a JS implementation is that it causes layout shifts for server-side rendered (SSR) pages. Since the server does not reliably know the dimensions of the client's device, the text needs to wait for the page to be hydrated before resizing. If supported in CSS, text would be able to render at just the right size even on initial render of SSR'ed pages.

In addition, while performance is certainly a consideration, other expensive CSS features such as animating height also exist and the performance implications are well known. Given the benefits of a "FitText" feature, it would be nice to be able to support this and allow developers weigh the performance cost against the benefits for their use case.

jimmy-guo avatar Sep 20 '22 21:09 jimmy-guo

I want us to return to this issue — we now have inline-size containment, which could be used to solve the potential issues regarding the circularity.

The list of things a potential solution for “fit to width” text should handle the following, in my opinion:

  1. It should work only inside an element with size or inline-size containment.
  2. We need to have an ability to set a min/max font-size.
    • We could use the existing font-size as the minimum — this would guarantee a) readable font-size when too much content/too narrow context, b) better graceful degradation.
    • I'd argue that introducing a new property instead of using something like fit-width(8px, 48px) value for the font-size could be more preferable: easier to detect the intent, easier to fall back to the regular font-size when you'd forget the containment/when the feature is not supported.
    • I think it is better to have a single property that would trigger the fit-to-width and set the maximum. Better to encourage having some limit, and someone who don't want to be limited could still set it to an arbitrary big number as a work-around.
  3. Should work with multi-line text:
    • If multiple lines of text are present (with hard-breaks, like with <br/> or in pre context) the longest line should be used for this limit.
    • By default should fit as many text as can fit until meeting the min font-size, then wrap.
    • Maybe there could be an option to force the “fit-to-width” on all the wrapped lines until they fit again — but only as an option, as both behaviors have use-cases.
    • Optionally, not sure if easy to implement — should work nicely with text-wrap: balance. Logically, with the simplest form of balancing, it sounds not super complicated: calculate the initial wrapping opportunity based on the min font-size, then balance things using the text-wrap: balance, then bump the size either based of the longest line for everything, or for each line separately, based on the preference from the previous item.

kizu avatar Mar 23 '23 21:03 kizu

For those who could want to experiment with CSS-only way of achieving this, I just wrote an article about how we can use scroll-driven animations (at the moment available in Chrome Canary) to do just that — https://kizu.dev/fit-to-width-text/

Compared to what is proposed in this article, the main limitation of that method is the absence of the min value for the font-size, alongside overall hackiness of the method, so I would still want to see this implemented in CSS natively :)

kizu avatar Jun 21 '23 09:06 kizu

Would it be useful to introduce this as a function which could optionally accept a percentage arg signifying how much of the available space the text should span?

For any bounds need, we could just use the built-in clamp function along with a new “fit” keyword, like font-size: clamp(8px, fit(), 48px) where fit() would have a default arg value of 100%.

brandonmcconnell avatar Jun 21 '23 09:06 brandonmcconnell

I was reading this thread from the top and was thinking exactly that, that some kind of fit-to-width option inside clamp would be awesome.

Alternatively if we could specify the size of a font by its average width (like by the ch unit?), then it might get us a step closer to this functionality, even if not perfectly so.

sarajw avatar Jun 22 '23 18:06 sarajw

I want us to return to this issue — we now have inline-size containment, which could be used to solve the potential issues regarding the circularity.

I started a Codepen example to help solve this: https://codepen.io/nathanchase/pen/rNKqYoX

Could we somehow utilize ch or ic units inside a calc(), and then clamp the font-size based on the container inline-size?

nathanchase avatar Nov 15 '23 16:11 nathanchase

Hey, everyone. I just published an article about my new technique for achieving a fit-to-width text:

https://kizu.dev/fit-to-width/

Unlike my previous article, it does not use scroll-driven animations, and is purely based on container query length units and some calculations involving registered custom properties, so it now works in all recent versions of modern browsers.

The gist of how it works: by duplicating the text, we can measure the smallest version of the text (by measuring the space that remains if we subtract it from its container), and then use the ratio between it and its container to adjust the size to 100%.

While there are some potential limitations for this technique if the font will specify alternative display for the glyphs based on the size, the pros of this method should be enough to see if we could adapt it as a native CSS property.

The exact name and syntax are to be specified, but the gist of my proposal is following:

  1. The new property will accept something like a fill keyword that makes the text grow infinitely if it is smaller than the container.
  2. Optionally, it could accept the “max-font-size” value that could be used as the upper limit.
  3. There is no need to specify the lower limit — the initial font size of the element could be used for it (but this is debatable).
  4. If the fill keyword is present, the maximum intrinsic size of that element expands to fill all the available space.
  5. If an upper limit is specified, the maximum intrinsic size is equal to the maximum intrinsic size of the text if it was rendered with that size.
  6. The minimum intrinsic size of that text is equal to the minimum intrinsic size of the original font size.

There appears to be no circularity involved in the technique (as it works already everywhere), and given the general scope of it will be similar to text-wrap: balance (headers), it should not pose any performance challenges.

I'll be happy with any feedback, and I encourage browser vendors to prototype this natively: this feature is a very common need, and could be a quick win if we will be able to add it to the Web platform in a way similar to text-wrap: balance|pretty.

Without a native CSS property, the technique, while is possible, is very cumbersome and requires text duplication which, if implemented incorrectly, could lead to accessibility problems, so having a proper way of achieving this would be great.

kizu avatar Jul 19 '24 16:07 kizu

@kizu Awesome work! I could see this being an excellent use case for a Web Component to facilitate the extra HTML and hide it in a shadow DOM, as a stopgap until CSS supports this natively. I imagine it would be pretty trivial to create a Vue/React/Angular/Svelte/etc. component version of this as well.

Great writeup on your process and result. Thank you for sharing it!

nathanchase avatar Jul 20 '24 01:07 nathanchase