it-to-Width Text
What if I will tell you how we could solve fit-to-width text with pure CSS without any hardcoded parameters? Curiously, scroll-driven animations will allow us to do just that! Join me as I continue exploring the experimental implementations of the latest specs.
pdate from 2024
I discovered a new technique that works in all latest versions of major browsers: “Fit-to-Width Text: A New Technique” — it requires text duplication, but otherwise works very well. It even allows us to still use the optical axis of variable fonts! Check it out!
he Example
Let me start with the demonstration: if you’d look at the following example in the browserGo to a sidenote that supports scroll-driven animations, you would see how the text fits the width of its container. The example has contentEditable
, so you could play with it or resize the container or browser window to see it automatically adapt to it.
hy Scroll-Driven Animations?
You could wonder: how does it work? And how scroll-driven animations help us achieveGo to a sidenote it? We don’t even have anything that is scrollable here!
Let me walk you through this example: its code is not that long. The HTML is simple: a paragraph with multiple inline elements.
<p class="fit-to-width" contentEditable>
<span>What if I will tell you</span>
<span>how we could use</span>
<em>scroll-driven animations</em>
<strong>to solve</strong>
<span>fit-to-width text?</span>
</p>
CSS is more involved: I will show you the complete codeGo to a sidenote for this example and then would try to explain what each line does:
/* 1 */
@supports (animation-range: entry-crossing) {
.fit-to-width {
font-size: 13rem; /* 2 */
overflow: hidden; /* 3 */
scroll-snap-type: both mandatory; /* 4 */
/* 5 */
& > * {
inline-size: max-content; /* 6 */
line-height: 1; /* 7 */
transform-origin: 0 0; /* 8 */
animation: apply-text-ratio linear; /* 9 */
animation-timeline: view(inline); /* 10 */
animation-range: entry-crossing; /* 11 */
display: block; /* 12 */
scroll-snap-align: start; /* 4 */
contain: layout; /* 13 */
}
}
}
/* 9 */
@keyframes apply-text-ratio {
from {
transform: scale(0); /* 14 */
margin-block-end: -1lh; /* 15 */
}
}
-
I’m wrapping everything in a
@supports
, checking for theanimation-range: entry-crossing
(checking for theanimation-timeline
made the example break in the current Firefox Nightly). Without scroll-driven animations, the rest of the code does not make much sense and will look broken. -
The exact value for the
font-size
is not super important. However, we can think of it as “max-font-size”: our text won’t get bigger than this value, but it would shrink. -
We’re using
overflow: hidden
because we don’t want to scroll anything: we don’t want to haveauto
. But because we want to use scroll-driven animations, we can use thehidden
value, enabling them. That is expected, and this is one of the differences betweenhidden
vs.clip
values. -
Scroll snapping is optional: I use it because it makes a difference for a
contentEditable
element, preventing the potential scroll that could happen when typing. However, I would keep it even if there won’t be editable content, just in case. -
Regularly, I would use a class here, but because I’m using a
contentEditable
, it is more convenient to use an immediate descendant combinator here. -
By default, our nested elements would not go beyond the container. However, we want the content to go as far as possible beyond the overflow, which we can achieve with an intrinsic
max-content
. -
Not strictly necessary, but better to set the
line-height
to something. For multiple lines,1
could be a good value. Otherwise, we could make it bigger, allowing the font’s ascenders and descenders not to go beyond overflow. -
At a later point, we would apply a
transform
to our elements, so we want to make sure the origin would be at the start of them instead of their center. -
Here is the part where the “animation” part comes in. The only important place on this line is that we make it
linear
, removing the variation in scaling that can appear. Note that we have to mention this before the followinganimation-
properties — even though they’re not a part of our shorthand if it would come after them, it would reset them anyway. -
Here is where we get the “scroll-driven” part. We are using the
view()
timeline in aninline
direction, as our text would overflow in it due to theinline-size: max-content
on line 6. -
The important part! To achieve the desired behavior, we apply the
entry-crossing
animation-range
. With our elements overflowing the inline dimension of their container, this range would make it so that when each element is at the start of it, the progress of the animation would have a value equal to the ratio of the container’s inline dimension to the element’s inline dimension. Bramus made a nice visualizer tool that can help understand how different ranges work. -
Because we did use inline elements inside the paragraph wrapper, we have to make them have
display: block
. -
It is optional, but I found
contain: layout
help with some of the ways the element is rendered when testing it with thecontentEditable
. Certainly, a place I would need to investigate more, but not really related to this technique. -
The primary moving part of the solution that does the scaling: setting the “from” keyframe to have
scale(0)
, alongside our animation having theentry-crossing
range, makes our overflowing content scale to the value we want! -
A negative margin might not be necessary if we have just one line, as we could limit the container’s height instead (and potentially benefit from only applying a transform in an animation). However, because we’re using multiple lines and because
transform
makes the element keep its original place in the layout, we would want to adjust the vertical space that our line is taking. Conveniently, we can use the newlh
unit to do so and adjust the height accordingly, again using theentry-crossing
range.
imitations
I see the main limitation in how while we can set the max
value for the font-size
, we cannot get a min
one, so long lines could get as small as possible.
Next, because we use the transform
, if a font has special handling of different sizes, especially for smaller letters, these won’t be applied because we always render everything at a significantly bigger static size.
There might be other issues: note that this method is experimental, was not tested in production, and there is no guarantee that it will continue to work when the scroll-driven animations would land in every browser.
otential Native Feature
I would like to see this implemented natively and not rely on an unintended usage of scroll-driven animations, even though the final code is relatively straightforward.
There is an issue about this on CSSWG — I recommend liking it and providing your use cases and thoughts on the potential API and requirements of this feature.
inal Words
Fun fact: this is a “third-order” article. A few weeks back, I started writing a continuation of my article about anchor positioning. Then, when doing experiments for it, I created something using multiple different techniques. That led to me starting to write a second article about that experiment. As a part of this experiment, I used this method of getting the ratio of an element to its parent via scroll-driven animations. And then, the following morning, I randomly thought about another use case for this ratio technique — and now you’re reading this article.
That means you have at least two more articles coming your way. At least one of them would have a different usage for this “ratio from a suspended animation” technique — and until we would get an ability to strip units in calculations, which was added to the css-values-4
spec a while ago, but was not implementedGo to a sidenote in any browser yet. If you ever wanted to get ratios out of different lengths, try using this technique and let me know about the results!
I cannot wait for the scroll-driven animations to be available everywhere — it is fascinating what getting access to the information about the overflow and the scroll position in CSS allows us to do. Another recent example (though much hackier) was Johannes Odland coming up with the “Scroll-persisted State” technique.
I hope other browsers will start implementing this spec soon!
Published on with tags: #Scroll Driven Animations #Future CSS #Experiment #Typography #Practical #CSS