Skip to main content
Tilbake til bloggen
CSSContainer QueriesTypographyFrontendPerformance

Tilpass tekst til containeren med ren CSS — uten JavaScript

Alle skriver en JavaScript-hook som krymper font-size til teksten får plass i en container. Her er et fullt fungerende alternativ helt uten JS — med container query-enheter, clamp() og språkbevisst orddeling, inkludert kaskadefellen som får folk til å gi opp.

Publisert 24. juli 202611 min lesing

Store display-overskrifter har en irriterende vane: teksten som passer perfekt i designet ditt, flyter over så snart containeren blir smalere eller teksten blir lengre på et annet språk. I årevis har standardløsningen vært JavaScript — måle den rendrede teksten, sammenligne den med containeren og krympe skriften til den får plass. Nesten alle finner opp dette på nytt: en hjemmelaget useFitText-hook, en ResizeObserver-løkke eller et bibliotek som fitty, textFit eller react-textfit.

Denne artikkelen viser et fullt fungerende alternativ med null JavaScript: skrift som skalerer til containeren med CSS container query-enheter, pluss riktig linjedeling med orddeling. Ingen hooks, ingen måling, ingen layoutforskyvning etter hydrering.

JavaScriptet alle skriver

Mønsteret er alltid det samme. Render teksten i en basestørrelse, les bredden på teksten og bredden på forelderen, og hvis teksten er bredere — reduser skriftstørrelsen og prøv igjen. En minimal versjon ser slik ut:

useFitText.ts
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 kostnader:

  • Det kjører etter at komponenten er montert, så teksten tegnes først i feil størrelse og hopper deretter — en synlig layoutforskyvning og et slag mot CLS.
  • I rammeverk med server-rendering sender serveren én størrelse, og klienten korrigerer den etter hydrering, noe som kan gi et blink.
  • Det leverer JavaScript for noe som i bunn og grunn er et layout-anliggende.
  • Løkken som går 1px om gangen forårsaker flere re-renders og tvinger fram synkrone layout-lesninger — akkurat den typen arbeid du ikke vil ha på hovedtråden.

Hvorfor JavaScript var svaret — inntil nylig

Det er verdt å være ærlig om hvorfor disse hookene finnes. Inntil container queries kom i nettleserne, kunne CSS rett og slett ikke dimensjonere en skrift relativt til et element. Du kunne dimensjonere relativt til viewporten med vw, men en overskrift i et 400px bredt sidefelt og den samme overskriften i en 1200px bred hero ville få identisk vw-basert størrelse, fordi vw ikke vet noe om containeren.

Container query-lengdeenheter endret det. De ble trygge på tvers av nettlesere først rundt 2023, så all kode for skrifttilpasning skrevet før det hadde ingen CSS-mulighet og grep til JavaScript av nødvendighet. Den begrensningen er nå borte — og det er hele poenget med denne artikkelen.

Byggeklossen: container query-enheter

En container query-lengdeenhet er en prosentandel av en query-containers størrelse. Merk et element som container med container-type: inline-size, så kan etterkommerne bruke cqi — der 1cqi tilsvarer 1% av containerens inline-størrelse (bredden i horisontale 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;
}

Nå er den samme tittelen i et 400px-kort 40px, og i et 1200px-kort 120px — automatisk, uten måling. Dette er brikken som manglet før container queries: en skriftstørrelse som svarer på forelderen.

Begrense størrelsen med clamp()

cqi ville fortsette å vokse på enorme containere og krympe til ingenting på bittesmå. Pakk den inn i clamp() for å sette et gulv og et tak — vanligvis mobilstørrelsen din som minimum og desktopstørrelsen 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  */
  );
}

Les det som: vær flytende med containeren, men aldri under 2rem og aldri over 4.5rem. Mellomleddet (cqi-stigningen) styrer hvor raskt skriften skalerer; de to grensene er mobil- og desktopstørrelsene i designet ditt. Hvis de størrelsene 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 felle som koster deg en time: kaskaden

Her er fellen som får folk til å gi opp og konkludere: "container queries virker ikke". Containeren løses opp riktig, enheten er rett, og likevel endrer skriften aldri størrelse. Årsaken er nesten alltid kaskaden, ikke container queries.

Symptomet er lumskt villedende. Skriften din blir spikret fast på sitt maksimum i hver container, uansett hvor smal den er — som er nøyaktig hvordan cqi oppfører seg når den ikke finner en container og faller tilbake på viewporten. Så du går og sjekker det åpenbare: er containeren satt opp riktig? Du bekrefter at container-type: inline-size er der, du går gjennom DOM for å verifisere at forfaren virkelig er containeren, du leser elementets beregnede container-type og den sier inline-size, du måler bredden på containeren og den stemmer. Alt stemmer. Og skriften sitter fortsatt fast. Du leter på feil sted.

Den raskeste veien ut er å slutte å stole på din egen regel og bevise enheten uavhengig. Slipp inn en engangssonde i den samme containeren med ikke annet enn en rå cqi-verdi, 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 riktig, men det ekte elementet ditt forblir fastspikret, er det hele diagnosen: container queries virker perfekt, og noe annet overstyrer font-size-en din. Det er kaskaden — og synderen er som regel en bredere selektor du glemte.

Designsystemer setter ofte typografi på nøstede elementer med en regel som .title p, .title span { font-size: … }. Den selektoren har en spesifisitet på (0,1,1). Hvis fit-regelen din sikter mot det samme elementet med én enkelt klasse — .fit { … }(0,1,0) — vinner designsystem-regelen og spikrer fast skriftstørrelsen, slik at clamp()-en din stilltiende overstyres. Begge kan til og med ligge i det samme kaskadelaget, så layers redder deg ikke. Verre: den bredere regelen var sannsynligvis viewport-basert, som er grunnen til at størrelsen så ut som en viewport-fallback — du så bokstavelig talt en annen regel vinne.

Løsningen er å gi regelen din minst like høy spesifisitet. Å nøste den under container-klassen er enklest — .title .fit er (0,2,0), som slår .title 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 container query-skriftstørrelsen din ser ut til å bli ignorert, inspiser elementet og sjekk hvilken regel som faktisk setter <code>font-size</code>. Ni av ti ganger vinner en bredere designsystem-selektor kaskaden — selve container queryen er helt grei.

Problemet med lange ord — og hvorfor det ikke er en container query-svikt

Det er én ting cqi virkelig ikke kan: den skalerer skriftstørrelsen etter containerens bredde, ikke etter lengden på en bestemt streng. Ett enkelt udelelig ord kan fortsatt være bredere enn containeren ved den beregnede størrelsen. Tysk er den klassiske synderen — et ord som Fremdsprachenkenntnisse flyter gladelig over en smal boks selv ved en container-passende skriftstørrelse.

Dette er ikke en feil i tilnærmingen; det er dens ærlige grense. Den gamle JavaScript-hooken håndterte dette tilfellet ved å måle akkurat det ordet og krympe videre. I CSS håndterer du det annerledes — du lar ordet brytes i stedet for å krympe hele overskriften. To egenskaper gjør jobben:

.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 sikkerhetsnettet: hvis et ord ikke får plass på en linje, bryter nettleseren det i stedet for å la det flyte over. text-wrap: balance er finpussen — det jevner ut linjelengdene så en brutt overskrift ikke ender med et ensomt enkeltord.

Dele med bindestrek — automatisk, per språk

Å bryte et ord midt i uten bindestrek ser grovt ut. CSS kan sette inn en ekte bindestrek på et språklig gyldig punkt med hyphens: auto. Detaljen verdt å forstå: orddeling er språkbevisst. Nettleseren bruker orddelingsordboken for elementets språk, som den leser fra lang-attributtet.

.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 dokumentet ditt allerede setter språket på roten — <html lang="de"> for en tysk side — virker orddelingen bare, per språk, uten ekstra kabling. Tyske sider deles med den tyske ordboken, engelske med den engelske. Du får riktige bindestreker på hvert språk gratis, så lenge lang gjenspeiler innholdet.

Behold overflow-wrap: break-word ved siden. hyphens: auto håndterer ord den vet hvordan den skal dele; fallbacken fanger opp alt annet (URL-er, merkenavn, oppdiktede sammensetninger) så ingenting noensinne flyter over.

Å sette det sammen

Her er den komplette, avhengighetsfrie komponenten. Merk toelementsstrukturen: et ytre element som er containeren, og et indre element som leser cqi fra den. Et element kan ikke dimensjonere sin egen skrift ut fra sin egen container, så du trenger forelder/barn-delingen.

<h2 class="title" style="--size-desktop: 72; --size-mobile: 32">
  <span class="fit">Learn anything, beautifully</span>
</h2>
title.css
.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 funksjonen: en overskrift som skalerer med containeren, respekterer et mobilgulv og et desktoptak, aldri flyter over og deles med riktige bindestreker på hvilket som helst språk — og den leverer ikke en eneste byte JavaScript.

CSS mot JavaScript, ærlig talt

AspektJS fit-text-hook / -bibliotekRen CSS (cqi + clamp)
Reagerer på containerens breddeJa (via måling)Ja (via cqi)
Reagerer på nøyaktig strenglengdeJaNei — begrenses i stedet av overflow-wrap / hyphens
Layoutforskyvning etter lastingVanlig (mål så skaler)Ingen — riktig ved første paint
Virker ved SSR før hydreringNeiJa
Levert JavaScriptJaIngen
Kostnad på hovedtrådenReflows + re-rendersNull
Språkbevisst orddelingManuellInnebygd via hyphens: auto

Den eneste kolonnen der JavaScript fortsatt vinner, er nøyaktig tilpasning per streng — å tvinge en bestemt overskrift til nøyaktig én linje i den største størrelsen som får plass. Hvis den presise oppførselen er et hardt krav, er måling (JS, eller en SVG-<text> som skalerer til en viewBox) fortsatt eneste vei. Men for det store flertallet av display-overskrifter ser containerrelativ dimensjonering like bra eller bedre ut — uten noen av kostnadene.

Nettleserstøtte

Container query-lengdeenheter (cqi med slektninger) støttes i alle gjeldende evergreen-nettlesere, og har gjort det siden 2023. clamp(), overflow-wrap, hyphens og text-wrap: balance er også bredt tilgjengelige (text-wrap: balance er nyest og degraderes elegant til normal linjedeling der det mangler). For en fallback på svært gamle nettlesere holder en enkel font-size deklarert før clamp()-linjen.

Oppsummering

  • Skrifttilpasnings-JavaScriptet alle skriver, finnes hovedsakelig fordi CSS ikke kunne dimensjonere typografi per container før container queries kom.
  • container-type: inline-size + font-size: clamp(min, Ncqi, max) gir containerrelativ typografi med et mobilgulv og et desktoptak.
  • Hvis størrelsen ser ut til å bli ignorert, er det kaskaden — overgå spesifisiteten til brede designsystem-selektorer ved å nøste regelen din.
  • cqi skalerer etter containerens bredde, ikke strenglengde; bruk overflow-wrap: break-word og hyphens: auto så lange ord brytes pent i stedet for å flyte over.
  • hyphens: auto er språkbevisst gratis når dokumentets lang er riktig satt.
  • Ty til JavaScript bare når du virkelig trenger nøyaktig tilpasning med én linje per streng.

Det er nå publisert — to bittesmå, avhengighetsfrie pakker: @oleksiimazurenko/react-fit-text for React og @oleksiimazurenko/fit-text for den rammeverksagnostiske CSS-kjernen (kildekode på GitHub):

npm install @oleksiimazurenko/react-fit-text
import { 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>