Text mit reinem CSS an seinen Container anpassen — ohne JavaScript
Jeder schreibt einen JavaScript-Hook, der die font-size verkleinert, bis der Text in einen Container passt. Hier ist eine voll funktionsfähige Alternative ganz ohne JS — mit Container-Query-Einheiten, clamp() und sprachabhängiger Silbentrennung, samt der Kaskaden-Falle, an der viele aufgeben.
Große Display-Überschriften haben eine lästige Angewohnheit: Der Text, der in deinem Design perfekt passt, läuft über, sobald der Container schmaler wird oder der Text in einer anderen Sprache länger ausfällt. Jahrelang war die Standardlösung JavaScript — den gerenderten Text messen, mit seinem Container vergleichen und die Schrift verkleinern, bis sie passt. Fast jeder erfindet das neu: ein selbstgebauter useFitText-Hook, eine ResizeObserver-Schleife oder eine Bibliothek wie fitty, textFit oder react-textfit.
Dieser Artikel zeigt eine voll funktionsfähige Alternative mit null JavaScript: Schrift, die sich mit CSS-Container-Query-Einheiten an ihren Container anpasst, plus sauberer Zeilenumbruch mit Silbentrennung. Keine Hooks, kein Messen, kein Layout-Sprung nach der Hydration.
Das JavaScript, das jeder schreibt
Das Muster ist immer dasselbe. Den Text in einer Ausgangsgröße rendern, die Breite des Textes und die seines Elternelements auslesen, und wenn der Text breiter ist, die Schriftgröße verringern und es erneut versuchen. Eine minimale Version sieht so aus:
import { useLayoutEffect, useRef, useState } from "react";
// Shrinks font-size by 1px until the text fits its parent.
export function useFitText() {
const parentRef = useRef<HTMLElement>(null);
const textRef = useRef<HTMLElement>(null);
const [fontSize, setFontSize] = useState<number>();
useLayoutEffect(() => {
const parent = parentRef.current;
const text = textRef.current;
if (!parent || !text) return;
const current = parseFloat(getComputedStyle(text).fontSize);
if (parent.offsetWidth < text.offsetWidth) {
setFontSize(current - 1); // re-render, measure again
}
}, [fontSize]);
return { parentRef, textRef, fontSize };
}Es funktioniert, hat aber echte Kosten:
- Es läuft nach dem Mounten der Komponente, sodass der Text zunächst in der falschen Größe gezeichnet wird und dann springt — ein sichtbarer Layout-Sprung und ein Schlag für den CLS.
- In Frameworks mit Server-Rendering schickt der Server eine Größe, und der Client korrigiert sie nach der Hydration, was ein Aufblitzen verursachen kann.
- Es liefert JavaScript für etwas aus, das im Kern eine Layout-Angelegenheit ist.
- Die 1px-Schritt-für-Schritt-Schleife verursacht mehrere Re-Renders und erzwingt synchrone Layout-Lesevorgänge — genau die Art Arbeit, die man nicht auf dem Main-Thread haben will.
Warum JavaScript die Antwort war — bis vor Kurzem
Man sollte ehrlich sein, warum es diese Hooks gibt. Bis Container-Queries browserübergreifend verfügbar waren, konnte CSS eine Schrift schlicht nicht relativ zu einem Element dimensionieren. Man konnte relativ zum Viewport mit vw dimensionieren, aber eine Überschrift in einer 400px breiten Sidebar und dieselbe Überschrift in einem 1200px breiten Hero bekämen die identische vw-basierte Größe, weil vw nichts über den Container weiß.
Container-Query-Längeneinheiten haben das geändert. Sie wurden erst um 2023 herum browserübergreifend sicher nutzbar, sodass jeder vorher geschriebene Code zum Anpassen von Schrift keine CSS-Option hatte und aus Notwendigkeit zu JavaScript griff. Diese Einschränkung ist nun weg — und genau darum geht es in diesem Artikel.
Der Baustein: Container-Query-Einheiten
Eine Container-Query-Längeneinheit ist ein Prozentsatz der Größe eines Query-Containers. Markiere ein Element mit container-type: inline-size als Container, und seine Nachfahren können cqi verwenden — wobei 1cqi 1% der Inline-Größe des Containers entspricht (seiner Breite bei horizontalen Schreibrichtungen).
.card {
container-type: inline-size; /* this element is now a query container */
}
.card .title {
/* 10% of .card's width — scales with the container, not the viewport */
font-size: 10cqi;
}Jetzt ist derselbe Titel in einer 400px-Karte 40px groß und in einer 1200px-Karte 120px — automatisch, ohne Messen. Das ist der Baustein, der vor den Container-Queries fehlte: eine Schriftgröße, die auf das Elternelement reagiert.
Die Größe mit clamp() begrenzen
Rohes cqi würde auf riesigen Containern immer weiter wachsen und auf winzigen auf nichts schrumpfen. Verpacke es in clamp(), um eine Unter- und eine Obergrenze zu setzen — typischerweise deine Mobilgröße als Minimum und deine Desktop-Größe als Maximum:
.card {
container-type: inline-size;
}
.card .title {
font-size: clamp(
2rem, /* min — never smaller than the mobile size */
10cqi, /* fluid — scales with the container width */
4.5rem /* max — never larger than the desktop size */
);
}Lies es als: sei fließend mit dem Container, aber niemals unter 2rem und niemals über 4.5rem. Der mittlere Term (die cqi-Steigung) steuert, wie schnell die Schrift skaliert; die beiden Grenzen sind die Mobil- und Desktop-Schriftgrößen deines Designs. Wenn diese Größen in Custom Properties liegen, kannst du alles darüber steuern:
.title {
--size-desktop: 72;
--size-mobile: 32;
}
.title .fit {
font-size: clamp(
calc(var(--size-mobile) * 1px),
10cqi,
calc(var(--size-desktop) * 1px)
);
}Eine Falle, die dich eine Stunde kostet: die Kaskade
Hier ist die Falle, an der viele aufgeben und schlussfolgern: "Container-Queries funktionieren nicht." Der Container wird korrekt aufgelöst, die Einheit stimmt, und trotzdem ändert die Schrift nie ihre Größe. Die Ursache ist fast immer die Kaskade, nicht die Container-Queries.
Das Symptom ist hinterhältig irreführend. Deine Schrift bleibt in jedem Container auf ihrem Maximum festgenagelt, egal wie schmal er ist — was genau dem Verhalten von cqi entspricht, wenn es keinen Container findet und auf den Viewport zurückfällt. Also prüfst du das Naheliegende: Ist der Container richtig eingerichtet? Du bestätigst, dass container-type: inline-size da ist, du gehst das DOM durch, um zu verifizieren, dass der Vorfahr wirklich der Container ist, du liest das berechnete container-type des Elements aus und es sagt inline-size, du misst die Breite des Containers und sie stimmt. Alles passt. Und die Schrift klebt immer noch fest. Du suchst an der falschen Stelle.
Der schnellste Ausweg ist, deiner eigenen Regel nicht mehr zu vertrauen und die Einheit unabhängig zu beweisen. Setze eine wegwerfbare Sonde in denselben Container, die nichts als einen rohen cqi-Wert enthält, und miss sie:
// Inject a bare probe as a child of the same container:
const probe = document.createElement("span");
probe.style.fontSize = "10cqi";
container.append(probe);
getComputedStyle(probe).fontSize;
// 90px inside a 900px container, 36px inside 360px → cqi works fine.
probe.remove();Wenn die Sonde korrekt skaliert, dein echtes Element aber festgenagelt bleibt, ist das die ganze Diagnose: Die Container-Queries funktionieren einwandfrei, und etwas anderes überschreibt deine font-size. Es ist die Kaskade — und der Übeltäter ist meist ein breiterer Selektor, den du vergessen hast.
Design-Systeme setzen Typografie oft auf verschachtelten Elementen mit einer Regel wie .title p, .title span { font-size: … }. Dieser Selektor hat eine Spezifität von (0,1,1). Wenn deine Fit-Regel dasselbe Element mit einer einzelnen Klasse anspricht — .fit { … } mit (0,1,0) — gewinnt die Design-System-Regel und nagelt die Schriftgröße fest, sodass dein clamp() stillschweigend überschrieben wird. Beide können sogar in derselben Kaskadenschicht liegen, sodass Layer dich nicht retten. Schlimmer noch: Diese breitere Regel war wahrscheinlich viewport-basiert, weshalb die Größe wie ein Viewport-Fallback aussah — du hast buchstäblich gesehen, wie eine andere Regel gewinnt.
Die Lösung besteht darin, deiner Regel mindestens gleiche Spezifität zu geben. Sie unter der Container-Klasse zu verschachteln ist der einfachste Weg — .title .fit ist (0,2,0), was .title p mit (0,1,1) schlägt:
/* Loses to `.title p { font-size: … }` — same layer, higher specificity */
.fit { font-size: clamp(2rem, 10cqi, 4.5rem); }
/* Wins: (0,2,0) > (0,1,1) */
.title .fit { font-size: clamp(2rem, 10cqi, 4.5rem); }Wenn deine Container-Query-Schriftgröße ignoriert zu werden scheint, inspiziere das Element und prüfe, welche Regel tatsächlich <code>font-size</code> setzt. In neun von zehn Fällen gewinnt ein breiterer Design-System-Selektor die Kaskade — die Container-Query selbst ist in Ordnung.
Das Problem mit langen Wörtern — und warum es kein Versagen der Container-Queries ist
Es gibt eine Sache, die cqi wirklich nicht kann: Es skaliert die Schriftgröße nach der Breite des Containers, nicht nach der Länge einer bestimmten Zeichenkette. Ein einzelnes untrennbares Wort kann bei der berechneten Größe immer noch breiter sein als der Container. Das Deutsche ist der klassische Übeltäter — ein Wort wie Fremdsprachenkenntnisse läuft selbst bei container-angemessener Schriftgröße bereitwillig über eine schmale Box hinaus.
Das ist kein Fehler im Ansatz; es ist seine ehrliche Grenze. Der alte JavaScript-Hook behandelte diesen Fall, indem er genau dieses Wort maß und weiter verkleinerte. In CSS behandelst du es anders — du lässt das Wort umbrechen, statt die gesamte Überschrift zu verkleinern. Zwei Eigenschaften erledigen das:
.title .fit {
font-size: clamp(2rem, 10cqi, 4.5rem);
/* Break a too-long word instead of overflowing the box */
overflow-wrap: break-word;
/* Balance line lengths for nicer multi-line headings */
text-wrap: balance;
}overflow-wrap: break-word ist das Sicherheitsnetz: Wenn ein Wort nicht auf eine Zeile passt, bricht der Browser es um, statt es überlaufen zu lassen. text-wrap: balance ist der Feinschliff — es gleicht die Zeilenlängen aus, damit eine umgebrochene Überschrift nicht mit einem einsamen einzelnen Wort endet.
Umbrechen mit Bindestrich — automatisch, pro Sprache
Ein Wort mittendrin ohne Bindestrich umzubrechen, wirkt grob. CSS kann mit hyphens: auto einen echten Bindestrich an einer sprachlich gültigen Stelle einfügen. Der Haken, den man verstehen sollte: Silbentrennung ist sprachabhängig. Der Browser verwendet das Silbentrennungswörterbuch für die Sprache des Elements, die er aus dem lang-Attribut ausliest.
.title .fit {
font-size: clamp(2rem, 10cqi, 4.5rem);
hyphens: auto; /* insert a real hyphen at valid break points */
overflow-wrap: break-word; /* last-resort break for words with no valid point */
text-wrap: balance;
}Das Elegante daran: Wenn dein Dokument die Sprache bereits am Wurzelelement setzt — <html lang="de"> für eine deutsche Seite — funktioniert die Silbentrennung einfach, pro Sprache, ohne zusätzliche Verdrahtung. Deutsche Seiten trennen mit dem deutschen Wörterbuch, englische mit dem englischen. Du bekommst in jeder Sprache korrekte Silbentrennung gratis, solange lang den Inhalt widerspiegelt.
Behalte overflow-wrap: break-word daneben. hyphens: auto behandelt Wörter, die es zu trennen weiß; der Fallback fängt alles Übrige ab (URLs, Markennamen, erfundene Zusammensetzungen), sodass nichts jemals überläuft.
Alles zusammensetzen
Hier ist die vollständige, abhängigkeitsfreie Komponente. Beachte die Zwei-Element-Struktur: ein äußeres Element, das der Container ist, und ein inneres Element, das cqi daraus ausliest. Ein Element kann seine eigene Schrift nicht aus seinem eigenen Container dimensionieren, daher brauchst du die Eltern-Kind-Aufteilung.
<h2 class="title" style="--size-desktop: 72; --size-mobile: 32">
<span class="fit">Learn anything, beautifully</span>
</h2>.title {
/* The container the inner text scales against */
container-type: inline-size;
}
/* Nested selector so it out-specifies design-system typography rules */
.title .fit {
display: block;
/* Fluid between the mobile and desktop sizes, driven by container width */
font-size: clamp(
calc(var(--size-mobile) * 1px),
10cqi,
calc(var(--size-desktop) * 1px)
);
/* Never overflow: break long words, hyphenate by language, balance lines */
overflow-wrap: break-word;
hyphens: auto;
text-wrap: balance;
}Das ist das gesamte Feature: eine Überschrift, die mit ihrem Container skaliert, eine Mobil-Untergrenze und eine Desktop-Obergrenze respektiert, niemals überläuft und in jeder Sprache mit korrekter Silbentrennung umbricht — und sie liefert kein einziges Byte JavaScript aus.
CSS vs. JavaScript, ehrlich betrachtet
| Aspekt | JS-fit-text-Hook / -Bibliothek | Reines CSS (cqi + clamp) |
|---|---|---|
| Reagiert auf Container-Breite | Ja (per Messung) | Ja (per cqi) |
| Reagiert auf exakte Zeichenkettenlänge | Ja | Nein — stattdessen durch overflow-wrap / hyphens begrenzt |
| Layout-Sprung nach dem Laden | Häufig (messen, dann skalieren) | Keiner — korrekt beim ersten Paint |
| Funktioniert bei SSR vor der Hydration | Nein | Ja |
| Ausgeliefertes JavaScript | Ja | Keins |
| Kosten auf dem Main-Thread | Reflows + Re-Renders | Null |
| Sprachabhängige Silbentrennung | Manuell | Eingebaut via hyphens: auto |
Die einzige Spalte, in der JavaScript weiterhin gewinnt, ist das exakte Anpassen pro Zeichenkette — eine bestimmte Schlagzeile auf genau eine Zeile in der größten passenden Größe zu zwingen. Wenn dieses präzise Verhalten eine harte Anforderung ist, bleibt Messung (JS oder ein SVG-<text>, das auf eine viewBox skaliert) der einzige Weg. Für die überwältigende Mehrheit der Display-Überschriften sieht container-relative Dimensionierung jedoch genauso gut oder besser aus — ohne all die Kosten.
Browser-Unterstützung
Container-Query-Längeneinheiten (cqi und Verwandte) werden von allen aktuellen Evergreen-Browsern unterstützt, und das seit 2023. clamp(), overflow-wrap, hyphens und text-wrap: balance sind ebenfalls weithin verfügbar (text-wrap: balance ist das Neueste und fällt anmutig auf normalen Umbruch zurück, wo es fehlt). Für einen Fallback auf sehr alten Browsern genügt eine einfache font-size-Deklaration vor der clamp()-Zeile.
Fazit
- Das Schrift-Anpassungs-JavaScript, das jeder schreibt, existiert vor allem, weil CSS die Schrift nicht nach dem Container dimensionieren konnte, bis Container-Queries verfügbar wurden.
container-type: inline-size+font-size: clamp(min, Ncqi, max)liefert container-relative Schrift mit einer Mobil-Untergrenze und einer Desktop-Obergrenze.- Wenn die Größe ignoriert zu werden scheint, ist es die Kaskade — übertreffe die Spezifität breiter Design-System-Selektoren, indem du deine Regel verschachtelst.
cqiskaliert nach Container-Breite, nicht nach Zeichenkettenlänge; verwendeoverflow-wrap: break-wordundhyphens: auto, damit lange Wörter sauber umbrechen, statt überzulaufen.hyphens: autoist gratis sprachabhängig, wenn daslangdes Dokuments korrekt gesetzt ist.- Greife nur dann zu JavaScript, wenn du wirklich exaktes Anpassen mit einer Zeile pro Zeichenkette brauchst.
Es ist jetzt veröffentlicht — zwei winzige, abhängigkeitsfreie Pakete: @oleksiimazurenko/react-fit-text für React und @oleksiimazurenko/fit-text für den framework-agnostischen CSS-Kern (Quellcode auf GitHub):
npm install @oleksiimazurenko/react-fit-textimport { FitText } from '@oleksiimazurenko/react-fit-text'
import '@oleksiimazurenko/fit-text/style.css'
// No props needed — scales to its container with sensible defaults.
<FitText>Learn anything, beautifully</FitText>