Макет документації без фреймворків: липка бічна панель, вміст і права TOC з CSS Grid
Ваша сторінка документації виглядає нормально на скриншоті. Але в продакшені виникає: липка бічна панель, яка не тримається, панель змісту, що дрижить, і колонка контенту, яка або розтягується, як тягучка, або стискається в хаотичний вигляд.
Це польовий посібник зі створення трьохколонного макета документації з CSS Grid — ліва навігація, центральний контент, права панель змісту — без фреймворка і без типової істерики «чому sticky мене ненавидить».
Зміст
- Чому цей макет ламається в продакшені
- Ціль: три колонки, що поводяться передбачувано
- Основи CSS Grid: єдиний шаблон, що працює
- Правила для sticky: що тримає і що вбиває
- Контейнери прокрутки: тихий вбивця sticky
- Читабельна ширина контенту та типографічні обмеження
- Права панель змісту: розмітка, зсуви та активні стани
- Адаптивність без фреймворків
- Доступність: skip links, фокус, зменшена анімація
- Продуктивність і стабільність: layout thrash теж аварія
- Практичні завдання з командами (і як вибирати)
- Швидкий план діагностики
- Поширені помилки (симптом → корінь → виправлення)
- Чеклісти / покроковий план
- Три міні-історії з практики
- Факти та історичний контекст
- Поширені питання
- Наступні кроки, що дійсно допомагають
Чому цей макет ламається в продакшені
Трьохколонні сторінки документації ламаються через банальні причини, і саме тому вони так часто ламаються. Не тому, що CSS Grid не справляється — Grid чудово підходить — а тому, що сторінка навколо грида (хедери, контейнери прокрутки, правила overflow, підключені банери, cookie-повідомлення та «корисні» обгортки) непомітно змінює правила, від яких залежить позиціонування sticky.
Коли SRE каже «в staging працює», зазвичай мається на увазі «працює на моєму ноутбуці з одним вікном і без сторонніх скриптів». Сторінки документації ще гірші: маркетингові пікселі, віджети зворотного зв’язку та синтаксичні підсвітки приєднуються до вечірки. Кожен з них може ввести контейнер прокрутки або викликати перерахунок верстки під час скролу. Так з’являються класичні симптоми:
- Ліва навігація, яка перестає бути липкою на півдорозі.
- Права панель змісту, що перекриває футер або зникає за ним.
- Колонка контенту, яка прокручується по горизонталі через довгі рядки коду.
- Нервова прокрутка, коли скрипт занадто часто перераховує активний пункт TOC.
Правильний підхід — ставитися до макета як до продакшн-інфраструктури: чітка відповідальність, мінімум рухомих частин, вимірювана поведінка та відома модель відмов.
Ціль: три колонки, що поводяться передбачувано
Ми хочемо макет з такими властивостями:
- Ліва бічна панель (сайт-навігація), липка під верхнім хедером, незалежно прокручується при довгому вмісті.
- Основний контент з комфортною довжиною рядка та стійкими блоками коду.
- Права панель змісту (заголовки на сторінці), липка, прокручується, не краде фокус і не потребує фреймворка.
І хочемо, щоб це деградировало ввічливо: на вузьких вікнах згортається в одну колонку без лабіринту вкладених скролбарів.
height: 100vh + внутрішнім скролом для сторінок документації. Це ламає sticky і ускладнює поведінку анкорів.
Основи CSS Grid: єдиний шаблон, що працює
Є багато способів це реалізувати. Більшість з них крихкі. Надійний патерн: нехай саме body (або документ) володіє прокруткою, використовуйте Grid для колонок та position: sticky для двох бічних панелей. Обмежуйте ширини фіксованими колонками для сайдбарів і гнучкою центральною колонкою з minmax(0, 1fr).
Цей minmax(0, 1fr) — не прикраса. Без 0 центральна колонка може відмовлятися змінювати розмір через внутрішні розміри контенту, і ви отримаєте overflow. Це проблема з роду «чому мій grid ігнорує мою ширину».
Форма грида: [sidebar] [content] [toc]
Шаблон: grid-template-columns: 270px minmax(0, 1fr) 250px;
Тримайте сайдбари як звичайні елементи в потоці грида; не позиціонуйте їх абсолютно. Абсолютне позиціювання здається хитрим, поки не почнеш друкувати, враховувати динамічну висоту хедера або підтримувати безпечні зони на мобільних.
Приклад структури HTML
Мінімально, нудно, стабільно:
cr0x@server:~$ cat layout.html
<header>...sticky top bar...</header>
<div class="layout">
<nav>...left nav...</nav>
<main>...content...</main>
<aside>...right toc...</aside>
</div>
«Секрет» в тому, що ніхто тут не створює новий контекст прокрутки. Саме так sticky виживає в бою.
Правила для sticky: що тримає і що вбиває
position: sticky простий доти, доки не стає складним. Sticky працює відносно найближчого предка з прокруткою. Якщо предок має overflow, що створює скрол-контейнер (auto, scroll, на практиці іноді hidden), sticky стає відносним до того контейнера, а не до вікна. Іноді це бажано. Більшість разів для документації — ні.
Ось стабільна конфігурація:
- Прокрутка документа (без «внутрішньої прокрутки додатку»).
- Липкі сайдбари з
top: headerHeight + gap. - Сайдбари мають
max-heightіoverflow: auto, щоб прокручувався лише їхній вміст, коли він занадто довгий.
А ось класичні вбивці sticky:
- Предок з overflow: обгортка з
overflow: hiddenдля обрізання тіні, менеджер модалів або «виправлення» горизонтального переповнення. - Трансформи на предку:
transformі деякі комбінаціїfilter/will-changeможуть змінювати контексти й поведінку прокрутки в несподіваний спосіб. - Використання
height: 100vhз внутрішнім скролом: sticky стає відносним до внутрішнього контейнера прокрутки, а анкор-посилання приземляються не туди, куди треба.
Зсуви через хедер: не вгадуйте
Якщо у вас є липкий хедер, анкори приземлятимуться під ним. Задайте scroll-margin-top для заголовків і закрийте питання. Не використовуєте пусті offset-диви.
Приклад:
h2, h3 { scroll-margin-top: calc(var(--header-h) + 12px); }
Контейнери прокрутки: тихий вбивця sticky
Більшість багів зі sticky — не проблеми sticky. Це проблеми з контейнерами прокрутки. Контейнер прокрутки створюється, коли елемент обрізає overflow і має прокручувану область. У реальних кодових базах вони з’являються, бо хтось хотів:
- запобігти горизонтальній прокрутці від блоків коду (
overflow-x: hiddenна обгортці) - застосувати blur або тінь і обрізати її
- «повноекранний shell», щоб футер не рухався
- обгортка віртуалізації для результатів пошуку або віджету зворотного зв’язку
Ваша задача — знайти найближчого предка з прокруткою для липкого елемента. У Chrome DevTools це видно, але в продакшені потрібні повторювані перевірки. Ви навіть можете додати режим відладки CSS, що підсвічує елементи з overflow. Зробіть це як перемикач на етапі збірки.
Вкладена прокрутка: обирайте рівно одного переможця
Сторінки документації повинні мати рівно одну основну прокрутку: документ. Сайдбари можуть бути вторинними зонами прокрутки за потреби, але сторінка не повинна залежати від внутрішньої прокрутки для базової навігації.
Вкладена прокрутка ламає:
- відновлення позиції при переході назад/вперед у браузері
- анкор-посилання
- поведінку клавіші PageDown
- деякі очікування скрінрідерів
Жарт #1: Вкладені контейнери прокрутки — UI-еквівалент RAID 0: швидко перетворюють дрібні помилки на великі наслідки.
Читабельна ширина контенту та типографічні обмеження
Центральна колонка — це дім ваших читачів. Широкий екран не означає, що ви повинні його використовувати повністю. Тримайте ширину в символах, не в пікселях. Це сайт документації, не білборд.
Правила, що працюють:
- Задайте
max-widthдля потоку контенту, наприклад74ch—80ch. - Дозвольте блокам коду прокручуватися по горизонталі всередині себе. Не «виправляйте» overflow на зовнішніх обгортках.
- Використовуйте
min-width: 0(або Grid-варіантminmax(0, 1fr)), щоб центральна колонка могла зменшуватись.
Також: блоки коду — це навантаження сховища документації. Вони стрибкоподібні, непередбачувані і повні патологічних випадків типу нескінченних рядків. Ставтесь до них як до тесту продуктивності: обмежуйте, ізолюйте і вимірюйте.
Права панель змісту: розмітка, зсуви та активні стани
Права панель змісту корисна, коли вона тихо працює. Вона не повинна ставати другою системою навігації, що воює з основною. Тримайте її мінімальною:
- Включайте лише H2/H3. Пропустіть H4, якщо ви не пишете стандарти.
- Обрізайте довгі заголовки візуально, але залишайте повний текст для скрінрідерів через
aria-label, якщо потрібно. - Не розгортайте дерево автоматично під час прокрутки, якщо це викликає layout thrash.
Мішені анкорів: стабільні id і заголовки
Не генеруйте id на клієнті під час виконання, якщо можете уникнути. Це ламає вхідні посилання і робить diff-и шумними. Генеруйте id на етапі збірки (SSG, markdown-процесор або невеликий скрипт в pipeline). Якщо у вас немає етапу збірки, використовуйте детерміністичні правила, але тоді заголовки мають бути стабільними.
Підсвічування активного розділу: IntersectionObserver, а не ручна геометрія
Старі скрипти TOC використовували слухачі прокрутки і ручні перевірки getBoundingClientRect. Це працює доти, доки не починаєш оптимізувати. Коректний примітив — IntersectionObserver, створений саме для питань виду «цей заголовок в полі зору чи ні».
Тим не менше: можна взагалі пропустити підсвічування. Багато команд доставляють його, а потім місяцями налаштовують. Якщо у вас обмежений час, витратьте його на sticky та типографіку. Читачі пробачать пасивний TOC. Вони не пробачать TOC, що викликає лаг прокрутки.
Адаптивність без фреймворків
На вузьких вікнах трьохколонний макет перетворюється на фарс. Згорніть у одну колонку. Можна або:
- укласти навігацію, контент і TOC в стік (в такому порядку), або
- сховати правий TOC і помістити міні-TOC у верхню частину статті
Другий варіант часто кращий: менше зон прокрутки, менше шуму, менше помилкових натискань.
Використовуйте брейкпоінт біля того місця, де центральна колонка стає тісною — звичайно близько 1024px залежно від ширини сайдбарів. Після цього вважайте перегляд за «мобільний» і спростіть. Не робіть «дві колонки з плаваючим TOC», якщо вам не подобається дебагати рідкісні випадки в iOS Safari.
Доступність: skip links, фокус, зменшена анімація
Макети документації рясні навігацією. Якщо ви зробите складний липкий shell і забудете про користувачів клавіатури, ви випустите лабіринт без світла.
Skip links та ролі-орієнтири
Додайте «Пропустити до контенту» як перший фокусований елемент. Використовуйте семантичні елементи (<nav>, <main>, <aside>) і маркуйте їх aria-label, де це допомагає.
Поведінка фокусу і прокрутка
Коли користувач переходить табом по липкій бічній панелі, контури фокусу мають залишатися видимими і не обрізатися overflow. Якщо ваша навігація прокручувана (overflow: auto), переконайтесь, що сфокусовані елементи прокручуються у видиму область. Браузери зазвичай це роблять, але може зірватися при кастомному управлінні фокусом або якщо ви застосовуєте незвичні трансформи.
Зменшена анімація
Якщо додаєте плавну прокрутку, поважайте prefers-reduced-motion. Також врахуйте, що плавна прокрутка може здаватися «затриманою» на слабших пристроях. Надійність важливіша за атмосферу.
Продуктивність і стабільність: layout thrash теж аварія
Сторінки документації не повинні топити ноутбуки. Проте я бачив, як це траплялося, головно через:
- обробники scroll, що виконують важку роботу
- скрипти TOC, які постійно вимірюють верстку (
getBoundingClientRect) у кожному кадрі - синтаксичні підсвітки, що перезапускаються при зміні маршруту в SPA
- веб-шрифти, які викликають пізні перерахунки, що зміщують анкори
Два жорсткі правила:
- Не робіть читання та запису layout в одному кадрі у відповідь на скрол. Якщо доводиться — батчіть їх.
- Віддавайте перевагу нативним примітивам браузера (CSS sticky, IntersectionObserver) замість власного опитування.
“Hope is not a strategy.” — Gene Kranz
В термінах ops: ставте бюджет продуктивності на прокрутку. Якщо макет документації викликає довгі таски або повторні перерахунки layout, це з’явиться в RUM як «таємна повільність», яку важко відтворити.
Практичні завдання з командами (і як вибирати)
Ці завдання припускають, що у вас є локальне середовище, збірка (навіть ручний HTML) і хоча б статичний сервер. Команди ті, що ви справді виконуєте при налагодженні поведінки макета в різних середовищах. Кожне завдання містить: команду, що означає вивід, і яке рішення приймати.
Завдання 1: Запустіть сайт локально без кешування
cr0x@server:~$ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
Що це означає: Ви подаєте статичні файли без хитрощів service worker. Добра відправна точка.
Рішення: Спочатку відтворіть баг зі sticky тут. Якщо він виникає лише за вашим реальним CDN/проксі, шукайте додану розмітку/скрипти або інші заголовки/CSP.
Завдання 2: Перевірте MIME і тип вмісту (TOC-скрипти ламаються через неправильний MIME)
cr0x@server:~$ curl -I http://127.0.0.1:8080/index.html
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.11.2
Content-type: text/html
Content-Length: 24891
Last-Modified: Sat, 28 Dec 2025 10:11:41 GMT
Що це означає: Правильний MIME і стабільний Last-Modified. Деякі браузери дивно поводяться, якщо CSS або JS сервуються як text/plain.
Рішення: Якщо в продакшені MIME неправильний — виправте конфіг сервера перед налагодженням CSS. Інакше ви будете полювати за примарами.
Завдання 3: Підтвердіть, що колонки грида обчислені як очікувалось
cr0x@server:~$ node -e "console.log('Use DevTools: Elements → Computed → grid-template-columns')"
Use DevTools: Elements → Computed → grid-template-columns
Що це означає: Немає CLI кращого за DevTools для обчислених треків грида. Ви перевіряєте непередбачені переозначення.
Рішення: Якщо бачите щось на зразок 270px 1000px 250px, коли очікували, що центр буде флексувати, знайдіть правило, що видалило minmax(0, 1fr) або ввело min-content поведінку.
Завдання 4: Пошук правил overflow, що створюють контейнери прокрутки
cr0x@server:~$ rg -n "overflow\s*:\s*(auto|scroll|hidden)" ./styles
styles/app.css:41: overflow: hidden;
styles/layout.css:112: overflow: auto;
Що це означає: Ви знайшли потенційних вбивць sticky. overflow: hidden на предку — головний підозрюваний.
Рішення: Для кожного випадку перевірте, чи обгортає воно ваші липкі елементи. Якщо так — видаліть або звузьте область застосування (наприклад, лише для блоку коду, а не для всієї обгортки макета).
Завдання 5: Знайти несподівані обгортки від інструментів збірки
cr0x@server:~$ rg -n "layout|wrapper|container|shell" ./dist/index.html
52:<div class="app-shell">
53: <div class="page-wrapper">
Що це означає: Ваша «проста сторінка» сидить всередині app shell. Цей shell часто володіє прокруткою.
Рішення: Якщо .app-shell використовує height: 100vh + overflow: auto, або рефакторіть shell, або прийміть, що sticky стане внутрішнім і підкоригуйте очікування (анкори, відновлення прокрутки тощо). Для документації краще рефакторити.
Завдання 6: Перевірити, чи батьки використовують трансформи (можуть ламати fixed/sticky)
cr0x@server:~$ rg -n "transform\s*:" ./styles
styles/marketing.css:88: transform: translateZ(0);
styles/marketing.css:212: transform: scale(1.02);
Що це означає: Хтось застосував transform-хак для «плавності». Часто це cargo cult.
Рішення: Якщо трансформовані елементи обгортають макет — прибирайте transform або перемістіть його в дочірній елемент. Потім повторно тестуйте sticky. Не тримайте GPU-хаки без вимірюваних результатів.
Завдання 7: Підтвердити висоту хедера відповідно до очікуваного offset для sticky
cr0x@server:~$ rg -n "--header-h" ./styles
styles/app.css:17: --header-h: 56px;
Що це означає: Ви використовуєте CSS-змінну для висоти хедера. Добре. Тепер перевірте, що хедер справді 56px на всіх брейкпоінтах.
Рішення: Якщо хедер переноситься на менших екранах і стає вищим, оновіть --header-h адаптивно або уникайте залежності від фіксованого піксельного значення (наприклад, обчислюйте в макеті або використовуйте менші значення top і задавайте scroll-margin-top, що трохи перекриває).
Завдання 8: Піймати горизонтальний overflow від блоків коду
cr0x@server:~$ rg -n "pre\s*\{|code\s*\{|white-space|word-break|overflow-x" ./styles
styles/code.css:9: pre { overflow-x: auto; }
styles/code.css:10: pre { white-space: pre; }
Що це означає: Ваші блоки коду будуть прокручуватись внутрішньо — це правильно. Якщо натомість ви бачите overflow-x: hidden на зовнішніх обгортках, це запах проблеми.
Рішення: Тримайте контроль overflow на рівні блоків коду. Якщо обгортка макета ховає overflow, sticky може зламатися, а контури фокусу — обрізатись.
Завдання 9: Перевірити, що анкори існують і унікальні
cr0x@server:~$ rg -n "id=\"" ./dist/index.html | head
118:<h2 id="grid-core">CSS Grid core: the one template that works</h2>
156:<h2 id="sticky-rules">Sticky rules: what makes it stick (and what kills it)</h2>
Що це означає: ID існують. Тепер перевірте унікальність.
Рішення: Якщо ID повторюються, браузери будуть переходити до першого збігу, ваш TOC буде «неправильним», і налагодження стане некомфортним. Виправляйте на стадії генерації.
Завдання 10: Переконайтесь, що CSP не блокує TOC-скрипт (звично в корпоративних налаштуваннях)
cr0x@server:~$ curl -I https://docs.example.internal/ | rg -i "content-security-policy"
Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'self'
Що це означає: Інлайн-скрипти можуть бути заблоковані. Якщо ваш TOC залежить від інлайн JS — він не запуститься.
Рішення: Перенесіть скрипти у статичні файли з 'self', або додайте nonce/hash. Для документації віддавайте перевагу TOC без JS з опціональним покращенням.
Завдання 11: Виявити довгі таски на клієнті, пов’язані з прокруткою
cr0x@server:~$ google-chrome --enable-logging=stderr --v=1 2>&1 | head
[1228/101521.402:VERBOSE1:chrome_main_delegate.cc(764)] basic startup complete
[1228/101521.409:VERBOSE1:startup_browser_creator.cc(157)] Launched chrome
Що це означає: Це лише логи запуску; реальна робота — у записах Performance в DevTools. Проте запуск з логами допомагає, коли підозрюються проблеми GPU/композитора.
Рішення: Якщо в перформанс-записах видно повторні перерахунки layout під час прокрутки — аудитуйте TOC-скрипти і будь-які слухачі прокрутки. Замініть їх на IntersectionObserver або тротлте до адекватних інтервалів.
Завдання 12: Переконатися, що service worker не кешує старі CSS/JS
cr0x@server:~$ rg -n "serviceWorker|navigator\.serviceWorker" ./dist
dist/app.js:14:navigator.serviceWorker.register("/sw.js")
Що це означає: Service worker існує. Він може закріпити старий CSS, викликаючи «виправлено, але не для всіх» поведінку.
Рішення: Для документації подумайте про відмову від service worker, якщо вам не потрібен офлайн. Якщо залишаєте — реалізуйте busting кешу і чіткий flow оновлення.
Завдання 13: Перевірити HTTP кешування CSS (застарілий CSS викликає фантомні регресії)
cr0x@server:~$ curl -I https://docs.example.internal/assets/app.css | rg -i "cache-control|etag|last-modified"
Cache-Control: public, max-age=31536000, immutable
ETag: "a8b3c-5f1c2d9b"
Що це означає: Імм’ютебл-кешування прийнятне лише якщо імена файлів залежать від вмісту (hash).
Рішення: Якщо ім’я файлу статичне (наприклад app.css), вимкніть immutable caching або додайте хеші. Інакше виправлення макета не потраплять до користувачів.
Завдання 14: Перевірити, що HTML має один main-орієнтир
cr0x@server:~$ tidy -errors -q ./dist/index.html | head
line 1 column 1 - Warning: missing
declaration
line 54 column 5 - Warning: should not appear as a child of
Що це означає: Проблеми структури HTML можуть збивати з пантелику допоміжні технології і іноді ваші CSS-селектори. (І ще: додайте doctype.)
Рішення: Виправляйте попередження структури, що впливають на орієнтири та заголовки. Не женіться за косметичними попередженнями, якщо вони не викликають реальних проблем.
Завдання 15: Аудит ієрархії заголовків (якість TOC від цього залежить)
cr0x@server:~$ rg -n "
Що це означає: Заголовки існують і виглядають впорядковано. Якщо ви бачите h4 перед h2, ваш генератор TOC зробить дурниці.
Рішення: Встановіть правила порядку заголовків в інструкціях для авторів і перевірки в CI. Дисципліна контенту окупається.
Швидкий план діагностики
Коли макет зламався і хтось питає «це баг в CSS?», часу на вгадування немає. Потрібен короткий план, що швидко знайде вузьке місце.
Перш за все: підтвердьте, хто володіє прокруткою
- Перевірка: чи прокручується сторінка на
body/html, або є внутрішня обгортка з власним скролом? - Чому: sticky поводиться відносно найближчого скрол-контейнера.
- Дія: якщо є внутрішній скрол-контейнер — вирішіть, чи прибрати його (бажано для документації) або підлаштувати sticky і зсуви анкорів відповідно.
Друге: знайдіть найближчого предка з overflow/transform
- Перевірка: інспектуйте сайдбар і TOC; піднімайтесь по DOM у пошуках
overflow,transform,filter,contain. - Чому: будь-яке з цих правил може змінити контекст для sticky або обрізати його.
- Дія: прибирайте або звужуйте сферу застосування; не застосовуйте налаштування overflow на рівні всієї обгортки макета без виправдання.
Третє: перевірте розміри треків грида і поведінку min-width
- Перевірка: переконайтесь, що центральна колонка —
minmax(0, 1fr)і що контент або його діти не маютьmin-width, що змушує розширюватися. - Чому: внутрішня мінімальна ширина від блоків коду і довгих рядків зруйнує ваш макет.
- Дія: задайте
min-width: 0для дітей грида за потреби і ізолюйте overflow в блоках коду.
Четверте: виміряйте лаг прокрутки
- Перевірка: запишіть Performance-трейс під час прокрутки.
- Чому: TOC-скрипти часті винуватці.
- Дія: замініть слухачі прокрутки на IntersectionObserver або відключіть активне підсвічування, якщо не можете зробити його дешевим.
Поширені помилки (симптом → корінь → виправлення)
Ліва бічна панель зовсім не липка
Симптом: бічна панель прокручується як звичайний елемент.
Корінь проблеми: відсутній top на липкому елементі, або sticky застосовано не до потрібного вузла (наприклад, до внутрішнього списку замість панелі).
Виправлення: Застосуйте position: sticky і top: ... до контейнера сайдбару. Переконайтесь, що він не всередині трансформованого предка.
Sticky працює до певного моменту, а потім відпускає
Симптом: сайдбар спочатку липкий, потім «відпускає».
Корінь проблеми: липкий елемент обмежений висотою його containing block (часто тому, що ряд грида або обгортка закінчується раніше, ніж ви думаєте).
Виправлення: Тримайте липкий елемент в тому самому контексті прокрутки, що й документ. Уникайте обгорток з overflow, що обрізають. Переконайтесь, що контейнер грида охоплює повну висоту контенту (зазвичай це відбувається природно, якщо не форсувати висоти).
Права панель змісту перекриває футер або кінець сторінки
Симптом: панель TOC сидить поверх футера.
Корінь проблеми: використано position: fixed замість sticky, або обгортка макета не резервує місце для TOC.
Виправлення: Використовуйте sticky всередині клітинки грида, щоб вона природно закінчувалась разом з контентом. Уникайте fixed, якщо справді не хочете плаваючу панель.
Колонка контенту викликає горизонтальну прокрутку всієї сторінки
Симптом: сторінку можна прокручувати вбік. Це ненавидять усі.
Корінь проблеми: довгі рядки коду або нескінченні рядки у поєднанні з треком грида, що не може зменшитись, бо середня колонка — 1fr без minmax(0, ...).
Виправлення: Використовуйте minmax(0, 1fr). Додайте overflow-x: auto на pre. Не ставте overflow-x: hidden на зовнішні обгортки як «вирішення» проблеми.
Анкори приземляються під хедером
Симптом: клік по TOC приводить до заголовка, схованого під липким хедером.
Корінь проблеми: немає scroll-margin-top на заголовках.
Виправлення: Додайте scroll-margin-top з висотою хедера + відступ. Тримайте це в CSS, а не в JavaScript.
Підсвічування активного пункту TOC відстає або мерехтить
Симптом: підсвічування стрибає між заголовками або оновлюється з запізненням.
Корінь проблеми: слухач прокрутки робить занадто багато роботи, або пороги неправильно налаштовані; також часто через змінну висоту хедера.
Виправлення: Використовуйте IntersectionObserver і виберіть адекватний rootMargin (наприклад, відступ зверху, що відповідає хедеру). Або відмовтесь від активного підсвічування.
Прокрутка сайдбару краде колеса миші
Симптом: користувач прокручує над сайдбаром і «залипає» на прокрутці навігації.
Корінь проблеми: сайдбари прокручувані й перехоплюють події wheel/touch.
Виправлення: Прийміть це як нормальну поведінку, але робіть сайдбари коротшими там, де можливо; розгляньте колапсовані секції. Уникайте агресивних методів проти scroll chaining, якщо не знаєте, що робите.
Друк виходить нечитабельним або відсіченим
Симптом: при друці сторінка включає навігацію/TOC або обрізає контент.
Корінь проблеми: липкі панелі і фони не опрацьовані для друку.
Виправлення: Додайте @media print, щоб сховати nav/aside і прибрати важкі фони та тіні.
Чеклісти / покроковий план
Покроково: будування макета (логічний порядок)
- Почніть з семантичного HTML:
header,nav,main,aside. Жодних обгорток без потреби. - Створіть грід: фіксовані лівий і правий треки; центр з
minmax(0, 1fr). - Задайте обмеження читабельності: обмежте довжину рядка через
max-widthна потоці контенту; зробіть блоки коду прокручуваними всередині. - Додайте поведінку sticky: липкі ліві та праві панелі з
topзалежно від висоти хедера; використовуйтеmax-height+overflow: autoдля довгих списків. - Виправте зсув анкорів:
scroll-margin-topна заголовках. - Адаптивне згортання: одна колонка під брейкпоінтом; розгляньте приховування правого TOC.
- Пас доступності: skip link, стилі фокусу, мітки орієнтирів, навігація клавіатурою.
- Пас продуктивності: прибрати слухачі scroll; якщо підсвічування активних заголовків — використовувати IntersectionObserver.
- Харденінг для продакшну: перевірки на додані обгортки/скрипти; закріпіть overflow і трансформи навколо макета.
Операційний чекліст: що переглядати в PR
- Ніякого нового
overflow: hiddenна високорівневих обгортках без виправдання і скріншотів на брейкпоінтах. - Ніяких нових «height: 100vh» shell для сторінок документації (якщо не готові прийняти наслідки і документувати їх).
- Grid-шаблон зберігає
minmax(0, 1fr)(або еквівалентmin-width: 0на контенті). - Заголовки мають стабільні id; немає дублікатів.
- Стилі для друку існують і ховають навігаційний шум.
- Будь-який JS для TOC опціональний і не ламає роботу без JS.
Три міні-історії з практики
Міні-історія 1: Інцидент через хибне припущення
Команда, з якою я працював, викотила редизайн документації всередині «уніфікованого app shell». Shell мав фіксований хедер, лівий сайдбар і внутрішню область контенту з height: 100vh і overflow: auto. Припущення було просте: «так роблять сучасні аплікації, і sticky працюватиме всередині».
Він працював. У Chrome. На десктопах. У щасливому шляху, коли висота хедера ніколи не змінювалась.
А потім зверху інжектували корпоративний банер для повідомлення про відповідність. Висота хедера збільшилась. Анкор-переходи почали приземлятись під хедером. Права панель змісту (що використовувала слухач прокрутки) порахувала неправильні зсуви, бо припускала, що корінь прокрутки — вікно, а не внутрішній контейнер. Користувачі кликали заголовки і думали, що контент зник. Підтримка почала надсилати скриншоти «порожніх секцій», що насправді були ховані вище фолду.
Інцидент не був аварією. Він був гіршим: повільним підривом довіри. Інженери припинили покладатися на документацію. Вони запитували колег, що дорого обходиться.
Виправлення було нудним: прибрати внутрішній контейнер прокрутки, дозволити документу прокручуватись, встановити scroll-margin-top і зробити банер частиною нормального потоку, щоб макет адаптувався природно. Припущення «app shell універсальний» було коренем проблеми. Документація — перш за все контент, а не chrome.
Міні-історія 2: Оптимізація, що відкотилася
Інша організація захотіла «надзвичайно плавну прокрутку» на довгих сторінках. Хтось додав патерн: застосувати transform: translateZ(0) до основної оболонки, щоб примусити композитор, та will-change: transform до кількох панелей. Зміна супроводжувалась гарним демонстраційним відео.
В продакшені права TOC почала поводитись непослідовно. На деяких машинах sticky переставав працювати, коли оболонка ставала трансформованою. На інших — працював, але рендер тексту трохи змінювався (subpixel зміщення) і заголовки зсувалися настільки, що пороги IntersectionObserver почали мерехтіти. Підсвічування активного заголовка танцювало між секціями під повільною прокруткою. Нічого не говорить «якість інженерії» так, як навігаційний віджет з кризою ідентичності.
Продуктивність також погіршилася на слабших пристроях. Браузер тримав додаткові шари через will-change, підвищуючи пам’ять і іноді викликаючи паузи GC. Користувачі описували сторінку як «тупу», що означає «оптимізація — причина проблеми».
Відкат був негайним. Довгострокове виправлення — дисципліна: утримувати трансформи від обгорток макета, вимірювати реальну продуктивність скролу і застосовувати will-change лише там, де анімація дійсно є. Більшість сторінок документації потребує передбачуваності, а не анімацій.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Одна команда мала нудну звичку: кожна зміна макета супроводжувалась простою тестовою сторінкою «інваріантів» в репозиторії. Нічого особливого. Один HTML-файл із патологічним контентом: дуже довгі рядки коду, глибоко вкладені заголовки, надмірна навігація, фейковий топ-банер і футер.
Перед релізом вони відкривали цю сторінку в кількох браузерах, змінювали розмір вікна і клікавали анкори. Це займало десять хвилин. Люди скаржилися ввічливо, як люблять інженери.
Потім оновлення стороннього віджета зворотного зв’язку обгорнуло всю сторінку в контейнер з overflow: hidden для своїх анімацій. Sticky могла зламатися. Але оскільки команда мала тестову інваріантну сторінку і перевірила її в staging з інтегрованими скриптами, регресія була виявлена до релізу.
Виправлення було не героїчним: звузити область віджета, щоб він не обгортав макет документації, і прибрати правило overflow з високорівневої обгортки. Нудна практика — тримати патологічну тестову сторінку і використовувати її — врятувала дні роботи з підтримкою і уникнула поломки навігації для всієї компанії.
Жарт #2: Єдина річ більш «липка», ніж position: sticky, — це багрепорт «docs navigation broken», зроблений за п’ять хвилин до фікс-релізу.
Факти та історичний контекст
- CSS Grid став широкодоступним у сучасних браузерах приблизно в 2017 році, замінивши десятиліття float-хаків і крихких колонних систем для багатьох макетів.
- Раніше «holy grail» трьохколонні макети часто будувалися з float або табличоподібних display режимів, що вимагали source-order трюків.
position: stickyз’явився як давноочікувана можливість, боfixedзанадто грубий для елементів в потоці, як сайдбари.- Поведінка sticky залежить від контейнерів прокрутки, а контейнери прокрутки стали частішими з SPA shell та бібліотеками компонентів, які за замовчуванням використовують внутрішню прокрутку.
- Патерн
minmax(0, 1fr)існує через правила внутрішнього розміру; без нього контент, як довгі рядки коду, може змусити треки грида ширшати за вікно. - Одиниці символів (
ch) — практичний інструмент типографіки для документації, бо вони масштабуються зі шрифтом і краще відображають довжину рядка, ніж пікселі. - IntersectionObserver введено, щоб зменшити зловживання слухачами прокрутки, давши платформі ефективний спосіб стежити за видимістю елементів.
- Сайти документації історично опиралися на server-rendered HTML, бо лінкування, друк і доступність — невід’ємні; важка клієнтська маршрутизація з’явилась пізніше і часто регресувала ці базові речі.
Поширені питання
1) Використовувати CSS Grid чи Flexbox для цього макета?
Використовуйте Grid. Це саме те, для чого Grid створений: дві фіксовані доріжки і одна гнучка з чіткими відступами. Flexbox може впоратись, але стає більш крихким при додаванні незалежно прокручуваних панелей і адаптивного згортання.
2) Чому ви наполягаєте на minmax(0, 1fr)?
Тому що дефолтний мінімум елементів грида може бути їхньою внутрішньою шириною. Довгі рядки коду змусать трек розширитись. minmax(0, 1fr) явно дозволяє треку зменшуватись.
3) Чи нормально робити всю сторінку shell з фіксованою висотою і внутрішньою прокруткою?
Можна, але ви купуєте проблеми: анкори, відновлення прокрутки, зсуви sticky і особливості мобільних браузерів. Для документації я рекомендую дозволити прокручуватись документу, якщо тільки у вас немає чіткої вимоги.
4) Мій sticky елемент працює, доки я не додам overflow: hidden. Чому?
Бо ви створили (або змінили) елемент, що визначає containing context для sticky. Правила overflow часто створюють такого предка або змінюють обрізання.
5) Чи генерувати праву панель змісту через JavaScript?
Якщо у вас є крок збірки — генеруйте на збірці. Якщо ні — тримайте простий статичний TOC або progressive enhancement. Не робіть JS обов’язковим для базової навігації документації, якщо не хочете потім це пояснювати.
6) Як запобігти занадто великій висоті TOC?
Дайте йому max-height: calc(100dvh - topOffset - bottomGap) і overflow: auto. Нехай він прокручується внутрішньо. Якщо все одно занадто масивний — у вас забагато заголовків; це питання контент-дизайну, а не CSS.
7) Як уникнути накладання TOC на контент на менших екранах?
Згорніть у одну колонку на брейкпоінті і або сховайте правий TOC, або перемістіть TOC у верхню частину статті. Намагатись зберегти три колонки на мобайлі — шлях до випадкової горизонтальної прокрутки і роздратування користувачів.
8) Чи шкодить position: sticky продуктивності?
Сам по собі sticky зазвичай безпечний. Убивці продуктивності — слухачі scroll, layout thrash і важкі ефекти рендерингу (тіні, фільтри) на великих липких панелях. Вимірюйте за допомогою трейсів; не вгадуйте.
9) Що з безпечними зонами і вирізами на мобільних?
Якщо у вас є липкий хедер, подумайте про використання environment-перемінних для safe-area-inset у паддінгах. Але тримайте це просто: на мобільних документація не повинна мати складний фіксований chrome.
10) Чи можна чисто підтримувати друк з цим макетом?
Так. Сховайте nav і TOC у @media print, приберіть фони і тіні, залиште підкреслені посилання. Друк все ще використовується для аудитів, рев’ю та інструкцій — на диво часто.
Наступні кроки, що дійсно допомагають
Якщо хочете, щоб макет пережив продакшн, зробіть нудні речі:
- Закріпіть модель прокрутки: прокрутка документа, а не внутрішній shell.
- Зробіть Grid стійким: фіксовані сайдбари,
minmax(0, 1fr)в центрі і уникайте сюрпризів внутрішніх розмірів. - Зробіть sticky передбачуваним: ніяких overflow/transform обгорток навколо липких елементів.
- Правильно фіксуйте анкори:
scroll-margin-topна заголовках, а не хакові диви. - Залишайте TOC як опціональне покращення: якщо підсвічування активного пункту викликає лаг — приберіть його. Ніхто не отримує pager через те, що TOC не світиться.
Потім виконайте ті самі перевірки, що й для сервісу: відтворіть локально, перевірте у середовищі, схожому на продакшн з інжектованими скриптами, і зберігайте патологічну тестову сторінку. Документація — це інфраструктура. Ставтеся до неї відповідно.