Lineare Skalierung der Schriftgröße mit CSS clamp() basierend auf dem Viewport

Avatar of Pedro Rodriguez
Pedro Rodriguez am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Responsive Typografie wurde in der Vergangenheit mit einer Vielzahl von Methoden wie Media Queries und CSS calc() versucht.

Hier werden wir eine andere Methode erkunden, um Text linear zwischen einer Reihe von minimalen und maximalen Größen zu skalieren, während die Breite des Viewports zunimmt. Ziel ist es, das Verhalten bei verschiedenen Bildschirmgrößen besser vorhersehbar zu machen – alles in einer einzigen CSS-Zeile, dank clamp().

Die CSS-Funktion clamp() ist ein Schwergewicht. Sie ist für eine Vielzahl von Dingen nützlich, aber besonders für die Typografie. So funktioniert sie. Sie nimmt drei Werte an: 

clamp(minimum, preferred, maximum);

Der Wert, den sie zurückgibt, ist der bevorzugte Wert, bis dieser bevorzugte Wert niedriger als der Mindestwert ist (dann wird der Mindestwert zurückgegeben) oder höher als der Höchstwert (dann wird der Höchstwert zurückgegeben).

In diesem Beispiel ist der bevorzugte Wert 50%. Auf der linken Seite des 400px Viewports sind 50% 200px, was weniger als der Mindestwert von 300px ist, der stattdessen verwendet wird. Auf der rechten Seite entsprechen 50% des 1400px Viewports 700px, was größer als der Mindestwert und kleiner als der Höchstwert von 800px ist, sodass es 700px entspricht.

Wäre es dann nicht immer der bevorzugte Wert, vorausgesetzt, Sie sind nicht seltsam und setzen ihn zwischen Minimum und Maximum? Nun, Sie erwarten wahrscheinlich, eine Formel für den bevorzugten Wert zu verwenden, wie

.banner {
  width: clamp(200px, 50% + 20px, 800px); /* Yes, you can do math inside clamp()! */
}

Angenommen, Sie möchten die minimale font-size eines Elements auf 1rem setzen, wenn die Viewport-Breite 360px oder weniger beträgt, und die maximale auf 3,5rem setzen, wenn die Viewport-Breite 840px oder mehr beträgt. 

Mit anderen Worten

1rem   = 360px and below
Scaled = 361px - 839px
3.5rem = 840px and above

Jede Viewport-Breite zwischen 361 und 839 Pixeln benötigt eine Schriftgröße, die linear zwischen 1 und 3,5rem skaliert wird. Das ist tatsächlich super einfach mit clamp()! Zum Beispiel erhalten wir bei einer Viewport-Breite von 600 Pixeln, genau in der Mitte zwischen 360 und 840 Pixeln, genau den mittleren Wert zwischen 1 und 3,5rem, nämlich 2,25rem.

Line chart with the vertical axis measured in font size rem unites from 0 to 4, and the horizontal axis measuring viewport width from 0 to 1,060 pixels. There are four blue points on the grid with a blue line connecting them.

Was wir mit clamp() erreichen wollen, nennt man lineare Interpolation: Zwischen zwei Datenpunkten Zwischeninformationen gewinnen.

Hier sind die vier Schritte, um dies zu tun

Schritt 1

Wählen Sie Ihre minimale und maximale Schriftgröße sowie Ihre minimale und maximale Viewport-Breite. In unserem Beispiel sind das 1rem und 3,5rem für die Schriftgrößen und 360px und 840px für die Breiten.

Schritt 2

Konvertieren Sie die Breiten in rem. Da 1rem in den meisten Browsern standardmäßig 16px beträgt (mehr dazu später), verwenden wir diesen Wert. Die minimalen und maximalen Viewport-Breiten betragen also nun 22,5rem bzw. 52,5rem.

Schritt 3

Hier leanen wir etwas zur mathematischen Seite. Wenn sie zusammengepaart werden, bilden die Viewport-Breiten und die Schriftgrößen zwei Punkte in einem X- und Y-Koordinatensystem, und diese Punkte bilden eine Linie.

A two-dimensional coordinate chart with two points and a red line intersecting them.
(22.5, 1) und (52.5, 3.5)

Wir brauchen irgendwie diese Linie – oder genauer gesagt ihre Steigung und ihren Schnittpunkt mit der Y-Achse. Hier ist, wie man das berechnet

slope = (maxFontSize - minFontSize) / (maxWidth - minWidth)
yAxisIntersection = -minWidth * slope + minFontSize

Das ergibt uns einen Wert von 0,0833 für die Steigung und -0,875 für den Schnittpunkt mit der Y-Achse.

Schritt 4

Jetzt bauen wir die clamp()-Funktion. Die Formel für den bevorzugten Wert lautet

preferredValue = yAxisIntersection[rem] + (slope * 100)[vw]

Die Funktion sieht also so aus

.header {
  font-size: clamp(1rem, -0.875rem + 8.333vw, 3.5rem);
}

Sie können das Ergebnis in der folgenden Demo visualisieren

Spielen Sie damit herum. Wie Sie sehen, hört die Schriftgröße auf zu wachsen, wenn die Viewport-Breite 840 Pixel erreicht, und hört auf zu schrumpfen bei 360 Pixeln. Alles dazwischen ändert sich linear.

Was passiert, wenn der Benutzer die Schriftgröße der Wurzel ändert?

Sie haben vielleicht einen kleinen Schönheitsfehler an diesem gesamten Ansatz bemerkt: Er funktioniert nur so lange, wie die Schriftgröße der Wurzel die ist, die Sie erwarten – im obigen Beispiel 16px – und sich nie ändert.

Wir konvertieren die Breiten, 360px und 840px, in rem-Einheiten, indem wir sie durch 16 dividieren, weil wir davon ausgehen, dass dies die Schriftgröße der Wurzel ist. Wenn der Benutzer seine Präferenzen auf eine andere Schriftgröße der Wurzel, z.B. 18px statt der Standard-16px, gesetzt hat, dann ist diese Berechnung falsch und der Text wird nicht wie erwartet skaliert.

Hier gibt es nur einen Ansatz, den wir verwenden können: (1) die notwendigen Berechnungen im Code beim Laden der Seite durchführen, (2) auf Änderungen der Schriftgröße der Wurzel hören und (3) alles neu berechnen, wenn Änderungen auftreten.

Hier ist eine nützliche JavaScript-Funktion für die Berechnungen

// Takes the viewport widths in pixels and the font sizes in rem
function clampBuilder( minWidthPx, maxWidthPx, minFontSize, maxFontSize ) {
  const root = document.querySelector( "html" );
  const pixelsPerRem = Number( getComputedStyle( root ).fontSize.slice( 0,-2 ) );

  const minWidth = minWidthPx / pixelsPerRem;
  const maxWidth = maxWidthPx / pixelsPerRem;

  const slope = ( maxFontSize - minFontSize ) / ( maxWidth - minWidth );
  const yAxisIntersection = -minWidth * slope + minFontSize

  return `clamp( ${ minFontSize }rem, ${ yAxisIntersection }rem + ${ slope * 100 }vw, ${ maxFontSize }rem )`;
}

// clampBuilder( 360, 840, 1, 3.5 ) -> "clamp( 1rem, -0.875rem + 8.333vw, 3.5rem )"

Ich lasse bewusst weg, wie der zurückgegebene String in CSS eingefügt wird, da es je nach Bedarf und ob Sie Vanilla-CSS, eine CSS-in-JS-Bibliothek oder etwas anderes verwenden, eine Fülle von Möglichkeiten gibt. Außerdem gibt es kein natives Ereignis für Schriftgrößenänderungen, sodass wir manuell danach suchen müssten. Wir könnten setInterval verwenden, um jede Sekunde zu prüfen, aber das könnte sich auf die Leistung auswirken.

Dies ist eher ein Randfall. Nur sehr wenige Leute ändern die Schriftgröße ihres Browsers und noch weniger werden sie ändern, während sie Ihre Seite besuchen. Aber wenn Sie möchten, dass Ihre Seite so reaktionsschnell wie möglich ist, dann ist dies der richtige Weg.

Für diejenigen, denen dieser Randfall nichts ausmacht

Update: Die hier geteilte Ressource funktioniert seit der Veröffentlichung dieses Artikels nicht mehr. Wenn Sie nach einem Rechner suchen, der Ihnen bei der Schriftgrößenberechnung für verschiedene Viewports hilft, sollten Sie den Fluid Type Generator in Betracht ziehen, der moderne Techniken für flüssige Typografie nutzt.

Wie man Textumbruch vermeidet

Eine so feingranulare Kontrolle über die Dimensionen der Typografie ermöglicht uns andere coole Dinge – wie das Stoppen des Textumbruchs bei verschiedenen Viewport-Breiten.

So verhält sich Text normalerweise.

Er hat eine bestimmte Anzahl von Zeilen bei einer bestimmten Viewport-Breite…
…und passt seine Zeilen an eine andere Breite an

Aber jetzt, mit der Kontrolle, die wir haben, können wir Text so gestalten, dass er die gleiche Anzahl von Zeilen behält, immer am selben Wort bricht, bei jeder Viewport-Breite, die wir ihm entgegenwerfen.

Viewport-Breite = 400px
Viewport-Breite = 740px

Wie machen wir das? Zunächst muss das Verhältnis zwischen Schriftgrößen und Viewport-Breiten gleich bleiben. In diesem Beispiel gehen wir von 1rem bei 320px zu 3rem bei 960px.

320 / 1 = 320
960 / 3 = 320

Wenn wir die clampBuilder()-Funktion verwenden, die wir zuvor erstellt haben, wird daraus

const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 320, 960, 1, 3 );

Es behält das gleiche Verhältnis von Breite zu Schrift. Der Grund dafür ist, dass wir sicherstellen müssen, dass der Text bei jeder Breite die richtige Größe hat, damit er die gleiche Anzahl von Zeilen behalten kann. Er wird bei verschiedenen Breiten immer noch umbrechen, aber dies ist notwendig für das, was wir als nächstes tun werden. 

Jetzt brauchen wir Hilfe von der CSS-Einheit ch, denn eine genau richtige Schriftgröße reicht nicht aus. Eine ch-Einheit entspricht der Breite des Zeichens "0" in der Schrift eines Elements. Wir wollen, dass der Textkörper so breit wie der Viewport ist, nicht indem wir width: 100% setzen, sondern mit width: Xch, wobei X die Anzahl der ch-Einheiten (oder 0en) ist, die benötigt werden, um den Viewport horizontal zu füllen.

Um X zu finden, müssen wir die minimale Viewport-Breite, 320px, durch die ch-Größe des Elements bei der jeweiligen Schriftgröße dividieren, wenn der Viewport 320px breit ist. Das ist in diesem Fall 1rem.

Keine Sorge, hier ist ein Code-Schnipsel zur Berechnung der ch-Größe eines Elements

// Returns the width, in pixels, of the "0" glyph of an element at a desired font size
function calculateCh( element, fontSize ) {
  const zero = document.createElement( "span" );
  zero.innerText = "0";
  zero.style.position = "absolute";
  zero.style.fontSize = fontSize;

  element.appendChild( zero );
  const chPixels = zero.getBoundingClientRect().width;
  element.removeChild( zero );

  return chPixels;
}

Nun können wir fortfahren, die Breite des Textes festzulegen

function calculateCh( element, fontSize ) { ... }

const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 320, 960, 1, 3 );
text.style.width = `${ 320 / calculateCh(text, "1rem" ) }ch`;
Ähm, wer hat dich zur Party eingeladen, Scrollbar?

Whoa, warte. Etwas Schlimmes ist passiert. Eine horizontale Scrollleiste vermasselt alles!

Wenn wir von 320px sprechen, meinen wir die Breite des Viewports, einschließlich der vertikalen Scrollleiste. Die Breite des Textes wird also auf die Breite des sichtbaren Bereichs plus die Breite der Scrollleiste gesetzt, wodurch er horizontal überläuft.

Warum also keine Metrik verwenden, die die Breite der vertikalen Scrollleiste nicht einschließt? Das können wir nicht, und das liegt an der CSS vw-Einheit. Denken Sie daran, wir verwenden vw in clamp(), um Schriftgrößen zu steuern. Sehen Sie, vw schließt die Breite der vertikalen Scrollleiste ein, wodurch die Schrift entlang der Viewport-Breite, einschließlich der Scrollleiste, skaliert wird. Wenn wir jeglichen Umbruch vermeiden wollen, muss die Breite proportional zu jeder Viewport-Breite sein, einschließlich der Scrollleiste.

Was machen wir also? Wenn wir das tun

text.style.width = `${ 320 / calculateCh(text, "1rem") }ch`;

…können wir das Ergebnis skalieren, indem wir es mit einer Zahl kleiner als 1 multiplizieren. 0,9 erledigt die Aufgabe. Das bedeutet, dass die Breite des Textes 90% der Viewport-Breite beträgt, was den kleinen Platz, der von der Scrollleiste eingenommen wird, mehr als ausgleicht. Wir können sie schmaler machen, indem wir eine noch kleinere Zahl verwenden, wie 0,6.

function calculateCh( element, fontSize ) { ... }

const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 20, 960, 1, 3 );
text.style.width = `${ 320 / calculateCh(text, "1rem" ) * 0.9 }ch`;
Auf Wiedersehen, Scrollbar!

Sie könnten versucht sein, einfach ein paar Pixel von 320 abzuziehen, um die Scrollleiste zu ignorieren, so etwa

text.style.width = `${ ( 320 - 30 ) / calculateCh( text, "1rem" ) }ch`;

Das Problem dabei ist, dass es das Umbruchproblem zurückbringt! Denn das Abziehen von 320 bricht das Viewport-zu-Schrift-Verhältnis.

Viewport-Breite = 650px
Viewport-Breite = 670px

Die Breite des Textes muss immer ein Prozentsatz der Viewport-Breite sein. Eine weitere Sache, die man beachten muss, ist, dass wir sicherstellen müssen, dass wir auf jedem Gerät, das die Website nutzt, die gleiche Schriftart laden. Das klingt offensichtlich, oder? Nun, hier ist ein kleines Detail, das Ihren Text durcheinander bringen könnte. Etwas wie font-family: sans-serif garantiert nicht, dass die gleiche Schriftart in jedem Browser verwendet wird. sans-serif setzt Arial unter Windows Chrome, aber Roboto unter Android Chrome. Auch die Geometrie einiger Schriften kann zu Umbrüchen führen, selbst wenn Sie alles richtig machen. Monospace-Schriften liefern tendenziell die besten Ergebnisse. Stellen Sie also immer sicher, dass Ihre Schriften auf den Punkt gebracht sind.

Schauen Sie sich dieses Beispiel ohne Umbruch in der folgenden Demo an

Nicht umbrechender Text in einem Container

Alles, was wir jetzt noch tun müssen, ist, die Schriftgröße und die Breite auf den Container statt direkt auf die Textelemente anzuwenden. Der Text darin muss nur auf width: 100% gesetzt werden. Dies ist in den Fällen von Absätzen und Überschriften nicht notwendig, da sie ohnehin Block-Level-Elemente sind und die Breite des Containers automatisch ausfüllen.

Ein Vorteil der Anwendung auf einen Eltercontainer ist, dass seine Kinder reagieren und sich automatisch anpassen, ohne dass ihre Schriftgrößen und Breiten einzeln gesetzt werden müssen. Außerdem, wenn wir die Schriftgröße eines einzelnen Elements ändern müssen, ohne die anderen zu beeinflussen, müssten wir nur seine Schriftgröße auf einen beliebigen em-Betrag ändern, und sie wird natürlich relativ zur Schriftgröße des Containers sein.

Nicht umbrechender Text ist wählerisch, aber es ist ein subtiler Effekt, der einem Design eine schöne Note verleihen kann!

Zusammenfassung

Um das Ganze abzurunden, habe ich eine kleine Demonstration zusammengestellt, wie all dies in einem realen Szenario aussehen könnte.

In diesem letzten Beispiel können Sie auch die Schriftgröße der Wurzel ändern, und die clamp()-Funktion wird automatisch neu berechnet, damit der Text in jeder Situation die richtige Größe hat.

Auch wenn das Ziel dieses Artikels die Verwendung von clamp() mit Schriftgrößen ist, könnte die gleiche Technik für jede CSS-Eigenschaft verwendet werden, die eine Längeneinheit annimmt. Nun, ich sage nicht, dass Sie das überall verwenden sollten. Oft reicht ein gutes altes font-size: 1rem aus. Ich versuche nur zu zeigen, wie viel Kontrolle Sie haben, wenn Sie sie brauchen.

Persönlich glaube ich, dass clamp() eines der besten Dinge ist, was CSS zu bieten hat, und ich kann es kaum erwarten zu sehen, welche anderen Verwendungen Leute dafür finden werden, da es immer weiter verbreitet wird!