Мәтінді контейнерге таза CSS-пен сыйғызу — JavaScript-сіз
Мәтін контейнерге сыйғанша font-size-ды кішірейтетін JavaScript hook-ты бәрі жазады. Міне, толық жұмыс істейтін, JS-сіз балама: container query бірліктері, clamp() және тілге байланысты тасымал. Көпшілік бас тартатын каскад тұзағын қоса.
Ірі display-тақырыптардың бір жағымсыз әдеті бар: дизайныңда мінсіз сыятын мәтін контейнер тарылған сәтте немесе басқа тілде жол ұзарған кезде асып кетеді. Жылдар бойы стандартты шешім JavaScript болды — рендерленген мәтінді өлшеп, контейнермен салыстырып, сыйғанша қаріпті кішірейту. Мұны әрбір адам дерлік қайта ойлап табады: үйде жасалған useFitText hook, ResizeObserver циклі немесе fitty, textFit, react-textfit сияқты кітапхана.
Бұл мақала нөл JavaScript-пен толық жұмыс істейтін баламаны көрсетеді: CSS container query бірліктерінің көмегімен қаріп өз контейнеріне қарай масштабталады, оған қоса сызықшамен дұрыс тасымал. Hook жоқ, өлшеу жоқ, гидратациядан кейін макет секіруі жоқ.
Бәрі жазатын JavaScript
Үлгі әрқашан бірдей. Мәтінді бір базалық өлшемде рендерле, мәтіннің енін және оның ата-элементінің енін оқы, егер мәтін кеңірек болса — қаріп өлшемін азайтып, қайта көр. Ең қарапайым нұсқасы былай көрінеді:
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 };
}Ол жұмыс істейді, бірақ нақты шығыны бар:
- Ол компонент монтаждалғаннан кейін іске қосылады, сондықтан мәтін алдымен қате өлшемде салынып, содан кейін секіреді — көзге көрінетін макет секіруі және CLS-ке соққы.
- Серверлік рендерингі бар фреймворктерде сервер бір өлшемді жібереді, ал клиент оны гидратациядан кейін түзетеді, бұл жыпылықтауға әкелуі мүмкін.
- Ол негізінен макетке қатысты нәрсе үшін JavaScript тиейді.
- 1px-ден 1px-ге дейінгі цикл бірнеше қайта рендерлеуді тудырады және синхронды макет оқуларын мәжбүрлейді — дәл негізгі ағында болғанын қаламайтын жұмыс түрі.
JavaScript неге жауап болды — жақынға дейін
Бұл hook-тар неге бар екенін адал айтқан жөн. Container queries браузерлерге келгенге дейін CSS қаріпті элементке қатысты өлшей алмайтын еді. vw арқылы viewport-қа қатысты өлшеуге болатын, бірақ 400px кең бүйір панельдегі тақырып пен 1200px кең hero-дағы дәл сол тақырып vw-ға негізделген бірдей өлшем алар еді, өйткені vw контейнер туралы ештеңе білмейді.
Container query ұзындық бірліктері мұны өзгертті. Олар браузерлер арасында тек 2023 жыл шамасында қауіпсіз қолжетімді болды, сондықтан бұған дейін жазылған қаріп сыйғызу коды үшін CSS нұсқасы болмады және амалсыздан JavaScript-ке жүгінді. Ол шектеу енді жоқ — мақаланың бүкіл мәні осында.
Негізгі құрылыс блогы: container query бірліктері
Container query ұзындық бірлігі — сұраныс контейнерінің өлшемінен пайыз. Элементті container-type: inline-size арқылы контейнер деп белгіле, сонда оның ұрпақтары cqi-ды қолдана алады — мұнда 1cqi контейнердің inline өлшемінің 1%-іне тең (көлденең жазу режимдерінде оның ені).
.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;
}Енді дәл сол тақырып 400px картада 40px, ал 1200px картада 120px — автоматты түрде, өлшеусіз. Бұл container queries-ке дейін жетіспеген бөлшек: ата-элементке жауап беретін қаріп өлшемі.
Өлшемді clamp() арқылы шектеу
Жалаң cqi алып контейнерлерде үздіксіз өсіп, кішкентайларда нөлге дейін кішірейер еді. Оны clamp()-қа орап, төменгі және жоғарғы шекараны қой — әдетте мобильді өлшемді минимум, ал десктоп өлшемін максимум ретінде:
.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 */
);
}Оны былай оқы: контейнермен бірге ағымды бол, бірақ ешқашан 2rem-нен төмен және 4.5rem-нен жоғары емес. Ортаңғы мүше (cqi көлбеуі) қаріптің қаншалықты тез масштабталатынын басқарады; екі шекара — дизайныңдағы мобильді және десктоп қаріп өлшемдері. Егер ол өлшемдер custom properties-те болса, бәрін солардан басқаруға болады:
.title {
--size-desktop: 72;
--size-mobile: 32;
}
.title .fit {
font-size: clamp(
calc(var(--size-mobile) * 1px),
10cqi,
calc(var(--size-desktop) * 1px)
);
}Саған бір сағатқа түсетін тұзақ: каскад
Міне, адамдарды бас тартуға және "container queries жұмыс істемейді" деп қорытуға мәжбүрлейтін тұзақ. Контейнер дұрыс шешіледі, бірлік дұрыс, бірақ қаріп өлшемін ешқашан өзгертпейді. Себебі әрдайым дерлік каскад, container queries емес.
Симптом айлакерлікпен алдамшы. Қаріпің әрбір контейнерде, қаншалықты тар болса да, өз максимумына қадалып қалады — бұл cqi контейнерді таппай, viewport-қа шегінгендегі мінез-құлқымен дәл сәйкес келеді. Сонда сен айқын нәрсені тексересің: контейнер дұрыс құрылған ба? container-type: inline-size бар екенін растайсың, ата-тек шынымен контейнер екенін тексеру үшін DOM-ды аралайсың, элементтің есептелген container-type-ын оқисың, ол inline-size дейді, контейнердің енін өлшейсің — дұрыс. Бәрі сәйкес. Ал қаріп әлі де қадалып тұр. Сен қате жерге қарап тұрсың.
Ең жылдам шығу жолы — өз ережеңе сенуді тоқтатып, бірлікті тәуелсіз түрде дәлелдеу. Дәл сол контейнерге жалаң cqi мәнінен басқа ештеңесі жоқ бір реттік зондты таста да, оны өлше:
// 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();Егер зонд дұрыс масштабталса, ал шынайы элементің қадалып қалса — бұл бүкіл диагноз: container queries мінсіз жұмыс істейді, ал басқа бір нәрсе сенің font-size-ыңды үстінен жазып тұр. Бұл каскад — ал кінәлі әдетте сен ұмытып кеткен кеңірек селектор.
Дизайн жүйелері жиі кірістірілген элементтерге .title p, .title span { font-size: … } сияқты ережемен типографияны орнатады. Ол селектордың ерекшелігі (0,1,1). Егер сенің fit-ережең бір ғана класпен дәл сол элементке бағытталса — .fit { … }, ерекшелігі (0,1,0) — дизайн жүйесінің ережесі жеңіп, қаріп өлшемін қадайды, сондықтан сенің clamp()-ың үнсіз үстінен жазылады. Екеуі тіпті бір каскад қабатында бола алады, сондықтан layer-лер сені құтқармайды. Одан да жаманы — ол кеңірек ереже, бәлкім, viewport-қа негізделген болатын, сондықтан өлшем viewport-fallback сияқты көрінді — сен нақты басқа ереженің жеңгенін көрдің.
Түзету — ережеңе кем дегенде тең ерекшелік беру. Оны контейнер класының астына кірістіру ең қарапайымы — .title .fit — (0,2,0), ол .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); }Егер container query қаріп өлшемің еленбейтіндей көрінсе, элементті тексеріп, <code>font-size</code>-ты шын мәнінде қай ереже орнататынын қара. Он жағдайдың тоғызында дизайн жүйесінің кеңірек селекторы каскадты жеңеді — ал container query-дың өзі дұрыс.
Ұзын сөз мәселесі — және неге бұл container query сәтсіздігі емес
cqi шынымен істей алмайтын бір нәрсе бар: ол қаріп өлшемін контейнердің еніне қарай масштабтайды, нақты жолдың ұзындығына емес. Бір бөлінбейтін сөз есептелген өлшемде әлі де контейнерден кеңірек болуы мүмкін. Неміс тілі — классикалық кінәлі: Fremdsprachenkenntnisse сияқты сөз контейнерге сай қаріп өлшемінде де тар қораптан оп-оңай асып кетеді.
Бұл тәсілдің багы емес; бұл оның адал шекарасы. Ескі JavaScript hook бұл жағдайды дәл сол сөзді өлшеп, әрі қарай кішірейтумен шешетін. CSS-те сен оны басқаша шешесің — бүкіл тақырыпты кішірейтудің орнына сөзді тасымалдауға мүмкіндік бересің. Екі қасиет жұмысты атқарады:
.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 — сақтандыру торы: егер сөз жолға сыймаса, браузер оны асып кетуге қалдырмай, үзеді. text-wrap: balance — жылтырату: ол жол ұзындықтарын теңестіреді, сонда тасымалданған тақырып жалғыз сөзбен аяқталмайды.
Сызықшамен үзу — автоматты, әр тілге
Сөзді сызықшасыз ортасынан үзу дөрекі көрінеді. CSS hyphens: auto арқылы лингвистикалық тұрғыда жарамды нүктеге нағыз сызықша қоя алады. Түсіну керек нюанс: тасымал тілге байланысты. Браузер элемент тілінің тасымал сөздігін пайдаланады, ал тілді lang атрибутынан оқиды.
.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;
}Ең тамашасы: егер құжатың тілді түбірде әлдеқашан орнатса — неміс беті үшін <html lang="de"> — тасымал жай ғана жұмыс істейді, әр локальге, қосымша сымсыз. Неміс беттері неміс сөздігімен, ағылшындары ағылшынмен тасымалданады. lang мазмұнды көрсетіп тұрғанша, әр тілде дұрыс сызықшаларды тегін аласың.
overflow-wrap: break-word-ты қатар сақта. hyphens: auto ол тасымалдай алатын сөздерді өңдейді; fallback қалғанының бәрін ұстайды (URL-дар, бренд атаулары, ойдан шығарылған біріккен сөздер), сонда ешнәрсе ешқашан асып кетпейді.
Бәрін біріктіру
Міне, тәуелділіксіз толық компонент. Екі элементті құрылымға назар аудар: контейнер болатын сыртқы элемент және одан cqi-ды оқитын ішкі элемент. Элемент өз қаріпін өз контейнерінен өлшей алмайды, сондықтан саған ата/бала бөлінісі керек.
<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;
}Міне, бүкіл мүмкіндік: контейнермен бірге масштабталатын, мобильді төменгі және десктоп жоғарғы шекараны құрметтейтін, ешқашан асып кетпейтін және кез келген тілде дұрыс сызықшалармен тасымалданатын тақырып — және ол бір байт та JavaScript тиемейді.
CSS пен JavaScript, шынын айтқанда
| Аспект | JS fit-text hook / кітапхана | Таза CSS (cqi + clamp) |
|---|---|---|
| Контейнер еніне жауап береді | Иә (өлшеу арқылы) | Иә (cqi арқылы) |
| Жолдың нақты ұзындығына жауап береді | Иә | Жоқ — оның орнына overflow-wrap / hyphens арқылы шектелген |
| Жүктелгеннен кейінгі макет секіруі | Жиі (өлшеп, содан кейін масштабтау) | Жоқ — бірінші paint-те дұрыс |
| Гидратацияға дейінгі SSR кезінде жұмыс істейді | Жоқ | Иә |
| Тиелген JavaScript | Иә | Жоқ |
| Негізгі ағын шығыны | Reflow + қайта рендерлеу | Нөл |
| Тілге байланысты тасымал | Қолмен | hyphens: auto арқылы кірістірілген |
JavaScript әлі жеңетін жалғыз баған — әр жолға нақты сыйғызу — белгілі бір тақырыпты сыятын ең үлкен өлшемде дәл бір жолға мәжбүрлеу. Егер бұл нақты мінез-құлық қатаң талап болса, өлшеу (JS немесе viewBox-қа масштабталатын SVG <text>) әлі де жалғыз жол. Бірақ display-тақырыптардың басым көпшілігі үшін контейнерге қатысты өлшемдеу дәл сондай жақсы немесе жақсырақ көрінеді — ешбір шығынсыз.
Браузер қолдауы
Container query ұзындық бірліктері (cqi және оның туыстары) барлық қазіргі evergreen браузерлерде қолдау табады, әрі 2023 жылдан бері солай. clamp(), overflow-wrap, hyphens және text-wrap: balance да кеңінен қолжетімді (text-wrap: balance ең жаңасы, ол жоқ жерде қалыпты тасымалға сыпайы төмендейді). Өте ескі браузерлердегі fallback үшін clamp() жолының алдында жарияланған қарапайым font-size жеткілікті.
Түйіндер
- Бәрі жазатын қаріп сыйғызу JavaScript-і негізінен CSS container queries шыққанға дейін типографияны контейнер бойынша өлшей алмағандықтан бар.
container-type: inline-size+font-size: clamp(min, Ncqi, max)мобильді төменгі және десктоп жоғарғы шекарамен контейнерге қатысты типография береді.- Егер өлшем еленбейтіндей болса — бұл каскад; ережеңді кірістіру арқылы дизайн жүйесінің кең селекторларының ерекшелігінен асып түс.
cqiжолдың ұзындығы бойынша емес, контейнердің ені бойынша масштабтайды; ұзын сөздер асып кетпей, ұқыпты тасымалдануы үшінoverflow-wrap: break-wordжәнеhyphens: autoқолдан.- Құжаттың
lang-ы дұрыс қойылғандаhyphens: autoтілге байланысты тегін жұмыс істейді. - JavaScript-ке тек саған шынымен "әр жолға бір жол" нақты сыйғызу керек болғанда ғана жүгін.
Енді ол жарияланды — тәуелділіксіз екі кішкентай пакет: React үшін @oleksiimazurenko/react-fit-text және фреймворкке тәуелсіз CSS-ядросы @oleksiimazurenko/fit-text (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>