Tilpas tekst til dens container med ren CSS — uden JavaScript
Alle skriver en JavaScript-hook, der skrumper font-size, indtil teksten passer i en container. Her er et fuldt fungerende alternativ helt uden JS — med container query-enheder, clamp() og sprogbevidst orddeling, inklusive kaskadefælden, der får folk til at give op.
Store display-overskrifter har en irriterende vane: teksten, der passer perfekt i dit design, flyder over, så snart containeren bliver smallere, eller teksten bliver længere på et andet sprog. I årevis har standardløsningen været JavaScript — måle den renderede tekst, sammenligne den med dens container og skrumpe skriften, indtil den passer. Næsten alle genopfinder dette: en hjemmelavet useFitText-hook, en ResizeObserver-løkke eller et bibliotek som fitty, textFit eller react-textfit.
Denne artikel viser et fuldt fungerende alternativ med nul JavaScript: skrift, der skalerer til dens container med CSS container query-enheder, plus korrekt linjeombrydning med orddeling. Ingen hooks, ingen måling, ingen layout-forskydning efter hydrering.
JavaScriptet, alle skriver
Mønsteret er altid det samme. Render teksten i en basestørrelse, læs bredden på teksten og bredden på dens forælder, og hvis teksten er bredere — reducer skriftstørrelsen og prøv igen. En minimal version ser sådan ud:
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 };
}Det virker, men har reelle omkostninger:
- Det kører efter, at komponenten er monteret, så teksten tegnes først i den forkerte størrelse og hopper derefter — en synlig layout-forskydning og et slag mod CLS.
- I frameworks med server-rendering sender serveren én størrelse, og klienten retter den efter hydrering, hvilket kan give et blink.
- Det leverer JavaScript til noget, der grundlæggende er et layout-anliggende.
- Løkken, der går 1px ad gangen, forårsager flere re-renders og tvinger synkrone layout-læsninger — præcis den slags arbejde, du ikke vil have på hovedtråden.
Hvorfor JavaScript var svaret — indtil for nylig
Det er værd at være ærlig om, hvorfor disse hooks findes. Indtil container queries kom i browserne, kunne CSS ganske enkelt ikke dimensionere en skrift i forhold til et element. Man kunne dimensionere i forhold til viewporten med vw, men en overskrift i et 400px bredt sidepanel og den samme overskrift i en 1200px bred hero ville få identisk vw-baseret størrelse, fordi vw ikke ved noget om containeren.
Container query-længdeenheder ændrede det. De blev først sikre på tværs af browsere omkring 2023, så al kode til skrifttilpasning skrevet før da havde ingen CSS-mulighed og greb til JavaScript af nødvendighed. Den begrænsning er nu væk — og det er hele pointen med denne artikel.
Byggestenen: container query-enheder
En container query-længdeenhed er en procentdel af en query-containers størrelse. Marker et element som container med container-type: inline-size, så kan dets efterkommere bruge cqi — hvor 1cqi svarer til 1% af containerens inline-størrelse (dens bredde i vandrette skriveretninger).
.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;
}Nu er den samme titel i et 400px-kort 40px, og i et 1200px-kort 120px — automatisk, uden måling. Dette er brikken, der manglede før container queries: en skriftstørrelse, der reagerer på forælderen.
Begræns størrelsen med clamp()
Rå cqi ville blive ved med at vokse på enorme containere og skrumpe til intet på bittesmå. Pak den ind i clamp() for at sætte et gulv og et loft — typisk din mobilstørrelse som minimum og din desktopstørrelse som maksimum:
.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 */
);
}Læs det som: vær flydende med containeren, men aldrig under 2rem og aldrig over 4.5rem. Mellemleddet (cqi-hældningen) styrer, hvor hurtigt skriften skalerer; de to grænser er mobil- og desktopstørrelserne i dit design. Hvis de størrelser ligger i custom properties, kan du styre alt derfra:
.title {
--size-desktop: 72;
--size-mobile: 32;
}
.title .fit {
font-size: clamp(
calc(var(--size-mobile) * 1px),
10cqi,
calc(var(--size-desktop) * 1px)
);
}En fælde, der koster dig en time: kaskaden
Her er fælden, der får folk til at give op og konkludere: "container queries virker ikke". Containeren opløses korrekt, enheden er rigtig, og alligevel ændrer skriften aldrig størrelse. Årsagen er næsten altid kaskaden, ikke container queries.
Symptomet er lumsk vildledende. Din skrift sidder naglet fast på sit maksimum i hver container, uanset hvor smal den er — hvilket er præcis, hvordan cqi opfører sig, når den ikke finder en container og falder tilbage på viewporten. Så du går og tjekker det oplagte: er containeren sat rigtigt op? Du bekræfter, at container-type: inline-size er der, du gennemgår DOM for at verificere, at forfaderen virkelig er containeren, du læser elementets beregnede container-type, og den siger inline-size, du måler containerens bredde, og den passer. Alt stemmer. Og skriften sidder stadig fast. Du leder det forkerte sted.
Den hurtigste vej ud er at holde op med at stole på din egen regel og bevise enheden uafhængigt. Smid en engangssonde ind i den samme container med intet andet end en rå cqi-værdi, og mål den:
// 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();Hvis sonden skalerer korrekt, men dit rigtige element forbliver naglet fast, er det hele diagnosen: container queries virker perfekt, og noget andet overskriver din font-size. Det er kaskaden — og synderen er som regel en bredere selektor, du glemte.
Designsystemer sætter ofte typografi på indlejrede elementer med en regel som .title p, .title span { font-size: … }. Den selektor har en specificitet på (0,1,1). Hvis din fit-regel sigter mod det samme element med en enkelt klasse — .fit { … } med (0,1,0) — vinder designsystem-reglen og nagler skriftstørrelsen fast, så din clamp() stiltiende overskrives. Begge kan endda ligge i det samme kaskadelag, så layers redder dig ikke. Værre endnu: den bredere regel var sandsynligvis viewport-baseret, hvilket er grunden til, at størrelsen lignede en viewport-fallback — du så bogstaveligt talt en anden regel vinde.
Løsningen er at give din regel mindst lige så høj specificitet. At indlejre den under container-klassen er nemmest — .title .fit er (0,2,0), som slår .title p på (0,1,1):
/* 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); }Hvis din container query-skriftstørrelse ser ud til at blive ignoreret, inspicer elementet og tjek, hvilken regel der faktisk sætter <code>font-size</code>. Ni ud af ti gange vinder en bredere designsystem-selektor kaskaden — selve container queryen er helt fin.
Problemet med lange ord — og hvorfor det ikke er en container query-fejl
Der er én ting, cqi virkelig ikke kan: den skalerer skriftstørrelsen efter containerens bredde, ikke efter længden af en bestemt streng. Et enkelt udeleligt ord kan stadig være bredere end containeren ved den beregnede størrelse. Tysk er den klassiske synder — et ord som Fremdsprachenkenntnisse flyder gladeligt over en smal boks selv ved en container-passende skriftstørrelse.
Dette er ikke en bug i tilgangen; det er dens ærlige grænse. Den gamle JavaScript-hook håndterede dette tilfælde ved at måle netop det ord og skrumpe videre. I CSS håndterer du det anderledes — du lader ordet blive brudt i stedet for at skrumpe hele overskriften. To egenskaber gør arbejdet:
.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 er sikkerhedsnettet: hvis et ord ikke kan være på en linje, bryder browseren det i stedet for at lade det flyde over. text-wrap: balance er finpudsningen — det udjævner linjelængderne, så en brudt overskrift ikke ender med et ensomt enkeltord.
Del med bindestreg — automatisk, per sprog
At bryde et ord midt over uden bindestreg ser groft ud. CSS kan indsætte en rigtig bindestreg på et sprogligt gyldigt punkt med hyphens: auto. Detaljen værd at forstå: orddeling er sprogbevidst. Browseren bruger orddelingsordbogen for elementets sprog, som den læser fra lang-attributten.
.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;
}Det elegante: hvis dit dokument allerede sætter sproget på roden — <html lang="de"> for en tysk side — virker orddelingen bare, per sprog, uden ekstra ledningsføring. Tyske sider deles med den tyske ordbog, engelske med den engelske. Du får korrekte bindestreger på hvert sprog gratis, så længe lang afspejler indholdet.
Behold overflow-wrap: break-word ved siden af. hyphens: auto håndterer ord, den ved, hvordan den skal dele; fallbacken fanger alt andet (URL'er, brandnavne, opdigtede sammensætninger), så intet nogensinde flyder over.
At samle det hele
Her er den komplette, afhængighedsfri komponent. Bemærk toelement-strukturen: et ydre element, der er containeren, og et indre element, der læser cqi fra den. Et element kan ikke dimensionere sin egen skrift ud fra sin egen container, så du har brug for forælder/barn-opdelingen.
<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;
}Det er hele funktionen: en overskrift, der skalerer med sin container, respekterer et mobilgulv og et desktoploft, aldrig flyder over og deles med korrekte bindestreger på ethvert sprog — og den leverer ikke en eneste byte JavaScript.
CSS mod JavaScript, ærligt talt
| Aspekt | JS fit-text-hook / -bibliotek | Ren CSS (cqi + clamp) |
|---|---|---|
| Reagerer på containerens bredde | Ja (via måling) | Ja (via cqi) |
| Reagerer på nøjagtig strenglængde | Ja | Nej — begrænses i stedet af overflow-wrap / hyphens |
| Layout-forskydning efter indlæsning | Almindeligt (mål så skaler) | Ingen — korrekt ved første paint |
| Virker ved SSR før hydrering | Nej | Ja |
| Leveret JavaScript | Ja | Ingen |
| Omkostning på hovedtråden | Reflows + re-renders | Nul |
| Sprogbevidst orddeling | Manuel | Indbygget via hyphens: auto |
Den eneste kolonne, hvor JavaScript stadig vinder, er nøjagtig tilpasning per streng — at tvinge en bestemt overskrift til nøjagtig én linje i den største størrelse, der kan være der. Hvis den præcise adfærd er et hårdt krav, er måling (JS eller en SVG-<text>, der skalerer til en viewBox) stadig eneste vej. Men for det store flertal af display-overskrifter ser containerrelativ dimensionering lige så god eller bedre ud — uden nogen af omkostningerne.
Browserunderstøttelse
Container query-længdeenheder (cqi med slægtninge) understøttes i alle nuværende evergreen-browsere, og det har de gjort siden 2023. clamp(), overflow-wrap, hyphens og text-wrap: balance er også bredt tilgængelige (text-wrap: balance er nyest og degraderer elegant til normal linjeombrydning, hvor det mangler). Til en fallback på meget gamle browsere er en simpel font-size erklæret før clamp()-linjen nok.
Opsummering
- Skrifttilpasnings-JavaScriptet, alle skriver, findes mest, fordi CSS ikke kunne dimensionere typografi per container, før container queries kom.
container-type: inline-size+font-size: clamp(min, Ncqi, max)giver containerrelativ typografi med et mobilgulv og et desktoploft.- Hvis størrelsen ser ud til at blive ignoreret, er det kaskaden — overgå specificiteten af brede designsystem-selektorer ved at indlejre din regel.
cqiskalerer efter containerens bredde, ikke strenglængde; brugoverflow-wrap: break-wordoghyphens: auto, så lange ord brydes pænt i stedet for at flyde over.hyphens: autoer sprogbevidst gratis, når dokumentetslanger sat korrekt.- Ty kun til JavaScript, når du virkelig har brug for nøjagtig tilpasning med én linje per streng.
Det er nu udgivet — to lillebitte, afhængighedsfrie pakker: @oleksiimazurenko/react-fit-text til React og @oleksiimazurenko/fit-text til den framework-agnostiske CSS-kerne (kildekode på 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>