First, what is line length? Line length is the length of a container that holds a body of multi-line text. “Multi-line” is the key part here, because text becomes less readable if the beginning of a line of text is too far away from the end of the prior line of text. This causes users to reread lines by mistake, and generally get lost while reading.
Luckily, the Web Content Accessibility Guidelines (WCAG) gives us a pretty hard rule to follow: no more than 80 characters on a line (40 if the language is Chinese, Japanese, or Korean), which is super easy to implement using character (ch
) units:
width: 80ch;
The width of 1ch
is equal to the width of the number 0
in your chosen font, so the exact width depends on the font.
Setting the optimal line length
Just because you’re allowed up to 80 characters on a line, it doesn’t mean that you have to aim for that number. A study by the Baymard Institute revealed that a line length of 50-75 characters is the optimal length — this takes into consideration that smaller line lengths mean more lines and, therefore, more opportunities for users to make reading mistakes.
That being said, we also have responsive design to think about, so setting a minimum width (e.g., min-width: 50ch
) isn’t a good idea because you’re unlikely to fit 50 characters on a line with, for example, a screen/window size that is 320 pixels wide. So, there’s a bit of nuance involved, and the best way to handle that is by combining the clamp()
and min()
functions:
clamp()
: Set a fluid value that’s relative to a container using percentage, viewport, or container query units, but with minimum and maximum constraints.min()
: Set the smallest value from a list of comma-separated values.
Let’s start with min()
. One of the arguments is 93.75vw
. Assuming that the container extends across the whole viewport, this’d equal 300px
when the viewport width is 320px
(allowing for 20px
of spacing to be distributed as you see fit) and 1350px
when the viewport width is 1440px
. However, for as long as the other argument (50ch
) is the smallest of the two values, that’s the value that min()
will resolve to.
min(93.75vw, 50ch);
Next is clamp()
, which accepts three arguments in the following order: the minimum, preferred, and maximum values. This is how we’ll set the line length.
For the minimum, you’d plug in your min()
function, which sets the 50ch
line length but only conditionally. For the maximum, I suggest 75ch
, as mentioned before. The preferred value is totally up to you — this will be the width of your container when not hitting the minimum or maximum.
width: clamp(min(93.75vw, 50ch), 70vw, 75ch);
In addition, you can use min()
, max()
, and calc()
in any of those arguments to add further nuance.
If the container feels too narrow, then the font-size
might be too large. If it feels too wide, then the font-size
might be too small.
Fit text to container (with JavaScript)
You know that design trend where text is made to fit the width of a container? Typically, to utilize as much of the available space as possible? You’ll often see it applied to headings on marketing pages and blog posts. Well, Chris wrote about it back in 2018, rounding up several ways to achieve the effect with JavaScript or jQuery, unfortunately with limitations. However, the ending reveals that you can just use SVG as long as you know the viewBox
values, and I actually have a trick for getting them.
Although it still requires 3-5 lines of JavaScript, it’s the shortest method I’ve found. It also slides into HTML and CSS perfectly, particularly since the SVG inherits many CSS properties (including the color, thanks to fill: currentColor
):
<h1 class="container">
<svg>
<text>Fit text to container</text>
</svg>
</h1>
h1.container {
/* Container size */
width: 100%;
/* Type styles (<text> will inherit most of them) */
font: 900 1em system-ui;
color: hsl(43 74% 3%);
text {
/*
We have to use fill: instead of color: here
But we can use currentColor to inherit the color
*/
fill: currentColor;
}
}
/* Select all SVGs */
const svg = document.querySelectorAll("svg");
/* Loop all SVGs */
svg.forEach(element => {
/* Get bounding box of <text> element */
const bbox = element.querySelector("text").getBBox();
/* Apply bounding box values to SVG element as viewBox */
element.setAttribute("viewBox", [bbox.x, bbox.y, bbox.width, bbox.height].join(" "));
});
Fit text to container (pure CSS)
If you’re hell-bent on a pure-CSS method, you are in luck. However, despite the insane things that we can do with CSS these days, Roman Komarov’s fit-to-width hack is a bit complicated (albeit rather impressive). Here’s the gist of it:
- The text is duplicated a couple of times (although hidden accessibly with
aria-hidden
and hidden literally withvisibility: hidden
) so that we can do math with the hidden ones, and then apply the result to the visible one. - Using container queries/container query units, the math involves dividing the inline size of the text by the inline size of the container to get a scaling factor, which we then use on the visible text’s
font-size
to make it grow or shrink. - To make the scaling factor unitless, we use the
tan(atan2())
type-casting trick. - Certain custom properties must be registered using the
@property
at-rule (otherwise they don’t work as intended). - The final
font-size
value utilizesclamp()
to set minimum and maximum font sizes, but these are optional.
<span class="text-fit">
<span>
<span class="text-fit">
<span><span>fit-to-width text</span></span>
<span aria-hidden="true">fit-to-width text</span>
</span>
</span>
<span aria-hidden="true">fit-to-width text</span>
</span>
.text-fit {
display: flex;
container-type: inline-size;
--captured-length: initial;
--support-sentinel: var(--captured-length, 9999px);
& > [aria-hidden] {
visibility: hidden;
}
& > :not([aria-hidden]) {
flex-grow: 1;
container-type: inline-size;
--captured-length: 100cqi;
--available-space: var(--captured-length);
& > * {
--support-sentinel: inherit;
--captured-length: 100cqi;
--ratio: tan(
atan2(
var(--available-space),
var(--available-space) - var(--captured-length)
)
);
--font-size: clamp(
1em,
1em * var(--ratio),
var(--max-font-size, infinity * 1px) - var(--support-sentinel)
);
inline-size: var(--available-space);
&:not(.text-fit) {
display: block;
font-size: var(--font-size);
@container (inline-size > 0) {
white-space: nowrap;
}
}
/* Necessary for variable fonts that use optical sizing */
&.text-fit {
--captured-length2: var(--font-size);
font-variation-settings: "opsz" tan(atan2(var(--captured-length2), 1px));
}
}
}
}
@property --captured-length {
syntax: "<length>";
initial-value: 0px;
inherits: true;
}
@property --captured-length2 {
syntax: "<length>";
initial-value: 0px;
inherits: true;
}
Watch for new text-grow
/text-shrink
properties
To make fitting text to a container possible in just one line of CSS, a number of solutions have been discussed. The favored solution seems to be two new text-grow
and text-shrink
properties. Personally, I don’t think we need two different properties. In fact, I prefer the simpler alternative, font-size: fit-width
, but since text-grow
and text-shrink
are already on the table (Chrome intends to prototype and you can track it), let’s take a look at how they could work.
The first thing that you need to know is that, as proposed, the text-grow
and text-shrink
properties can apply to multiple lines of wrapped text within a container, and that’s huge because we can’t do that with my JavaScript technique or Roman’s CSS technique (where each line needs to have its own container).
Both have the same syntax, and you’ll need to use both if you want to allow both growing and shrinking:
text-grow: <fit-target> <fit-method>? <length>?;
text-shrink: <fit-target> <fit-method>? <length>?;
<fit-target>
per-line
: Fortext-grow
, lines of text shorter than the container will grow to fit it. Fortext-shrink
, lines of text longer than the container will shrink to fit it.consistent
: Fortext-grow
, the shortest line will grow to fit the container while all other lines grow by the same scaling factor. Fortext-shrink
, the longest line will shrink to fit the container while all other lines shrink by the same scaling factor.
<fit-method>
(optional)scale
: Scale the glyphs instead of changing thefont-size
.scale-inline
: Scale the glyphs instead of changing thefont-size
, but only horizontally.font-size
: Grow or shrink the font size accordingly. (I don’t know what the default value would be, but I imagine this would be it.)letter-spacing
: The letter spacing will grow/shrink instead of thefont-size
.
<length>
(optional): The maximum font size fortext-grow
or minimum font size fortext-shrink
.
Again, I think I prefer the font-size: fit-width
approach as this would grow and shrink all lines to fit the container in just one line of CSS. The above proposal does way more than I want it to, and there are already a number of roadblocks to overcome (many of which are accessibility-related). That’s just me, though, and I’d be curious to know your thoughts in the comments.
Conclusion
It’s easier to set line length with CSS now than it was a few years ago. Now we have character units, clamp()
and min()
(and max()
and calc()
if you wanted to throw those in too), and wacky things that we can do with SVGs and CSS to fit text to a container. It does look like text-grow
and text-shrink
(or an equivalent solution) are what we truly need though, at least in some scenarios.
Until we get there, this is a good time to weigh-in, which you can do by adding your feedback, tests, and use-cases to the GitHub issue.
Setting Line Length in CSS (and Fitting Text to a Container) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
from CSS-Tricks https://ift.tt/tMyeu38
via IFTTT
No comments:
Post a Comment