Сайти документації обожнюють шторку навігації, що висувається. Користувачам вона теж подобається — поки не зачинить їх на сторінці, не змусить iPhone гальмувати або не зробить навігацію клавіатурою схожою на спуск у печеру без налобного ліхтаря.
Якщо ви експлуатуєте продакшн-системи (або просто відповідаєте на оповіщення про «документація неюзабельна на мобільних»), вам не потрібна хитра шторка. Вам потрібна нудна, передбачувана шторка: коректний оверлей, надійне блокування прокрутки та обробка фокусу, яка не ламається після кожного оновлення фреймворку.
Як виглядає «добре» в навігаційній шторці документації
Мобільна навігаційна шторка документації має три завдання, і більшість реалізацій провалює принаймні одне:
- Показати навігацію без втрати контексту: висувна бічна панель, чітка ієрархія, швидке закриття.
- Запобігти втручанню сторінки позаду: реальний оверлей, реальне блокування прокрутки, ніяких «фантомних» кліків.
- Повага до людського вводу: дотик, клавіатура, програми зчитування екрану, зменшена анімація та дивні краєвих випадків браузерів.
«Добре» означає, що наведені поведінки працюють на найгірших пристроях і в найгірших браузерах (привіт iOS Safari):
- Відкриття шторки: решта сторінки інертна (немає прокрутки, кліків або фокусу).
- Закриття шторки: сторінка повертається точно до попередньої позиції прокрутки, а фокус — до кнопки, що відкрила її.
- Клавіатура: Tab лишається всередині шторки; Esc закриває її; перемикач доступний для виявлення.
- Дотик: фон не «резиново» прокручується під оверлеєм; немає випадкового виділення тексту.
- Маршрутизація: навігація надійно закриває шторку і не залишає документ у заблокованому стані.
- Продуктивність: анімації не викликають трешу лейаута; оверлей не перетворює GPU на печку.
Думка: ставтеся до шторки як до модального вікна. Не зовні, а поведінкою. Якщо ви не дозволили б фону прокручуватися за модальним діалогом, не дозволяйте йому за навігаційною шторкою. Сторінки документації довгі, отже помилки голосніше проявляються.
Факти та історія, що пояснюють сучасні проблеми шторок
Розуміння шляху до цієї точки робить поточні баги менш таємничими й простішими для відтворення та виправлення.
- Іконка «гамбургер» не була винайдена для телефонів. Вона з’явилася в інтерфейсах 1980-х; мобільні лише зробили її відомою.
- Mobile Safari довго опирався «правильному» блокуванню прокрутки, бо скролінг оброблявся в спеціалізованому композитному шляху; веб-розробники боролися із цим хаком.
- Елементи з position: fixed на iOS раніше перетасовувались під час показу/приховування адресного рядка. Деякі крайові випадки досі проявляються як дрібні смикання.
- «100vh» історично брешуть на мобайлі, бо інтерфейс браузера динамічно займає простір; шторки, що припускають стабільну висоту вікна перегляду, отримують обрізання або дивні переповнення.
- Стекінг-контексти ускладнилися з розвитком CSS. Властивості як
transform,filterіopacityстворюють нові стекінг-контексти; оверлеї дивно з’являються під хедерами. - Веб роками не мав примітиву
inert, тож команди імітували його відключенням pointer events або перехопленням фокусу. Атрибутinertтепер широко доступний — але його все одно треба тестувати. - Гідрація фреймворку змінила профіль помилок. Серверний HTML, що стає інтерактивним пізніше, може коротко дозволити фонова прокрутка або фокус до підключення JS-обробників.
- Бекдроп-блюр став популярним через вигляд «преміум», а потім усі виявили, що на середньому пристрої це виглядає як падіння кадрів.
Цитата, яка варта місця на вашій стіні, бо шторки видаються «UI», але все ж частина надійності:
«Hope is not a strategy.» — General Gordon R. Sullivan
Він не про фокус-трапи говорив. Але міг би.
Архітектура: оверлей, панель і «одне джерело правди» для стану
Мінімальна архітектура, що працює надійно:
- Кнопка перемикання (зазвичай у хедері): керує станом; має чітку доступну назву; відображає відкрито/закрито.
- Затемнювач/оверлей: покриває вікно перегляду; перехоплює кліки/тапи; опціонально затемнює фон; механізм закриття.
- Панель шторки: оф-канвас елемент, що висувається; містить навігацію; має кнопку закриття вгорі.
- Менеджер стану: один булевий прапорець плюс трохи метаданих (який елемент був у фокусі перед відкриттям; позиція прокрутки).
Тримайте стан нудним. Ви не будуєте розподілену базу даних. Як тільки введете «частково відкрито», «тягнеться відкриття», «відкрито через hover», ви витратите вихідні дні на гонитву за крайовими випадками на пристроях, яких у вас немає.
Моделюйте шторку як модальне вікно (але не ускладнюйте)
Використовуйте невелику ідею state machine, навіть якщо реалізуєте її парою прапорців:
- Закрито: немає оверлею; прокрутка body нормальна; фокус нормальний.
- Відкривається: спочатку встановіть inert/блокування прокрутки, потім анімацію. Забороніть ввід під час переходу.
- Відкрито: трап фокусу; активний оверлей; закриття по кліку на оверлей, кнопці закриття, Esc та зміні маршруту.
- Закривається: деактивуйте трап після завершення анімації; відновіть прокрутку; відновіть фокус.
Порядок важливий. Якщо ви спочатку анімуєте, а потім блокуєте прокрутку, користувачі зможуть прокрутити сторінку під час анімації. Вони так і зроблять. Миттєво.
Короткий жарт, бо ми маємо справу зі станами UI
Є дві складні проблеми в інформатиці: інвалідизація кешу, називання речей, і закриття навігаційної шторки при зміні маршруту.
Оверлей і стекінг-контексти: чому бекдроп під хедером
Коли оверлей не покриває все, це майже ніколи не просто «z-index потрібно вищий». Це стекінг-контексти.
Як ви випадково створюєте стекінг-контекст
Будь-що з цього на предку може спричинити, що ваш оверлей поводитиметься так, ніби він за іншим UI:
transform(включно зtransform: translateZ(0)«хаком для продуктивності»)filter/backdrop-filteropacity < 1positionплюсz-indexв певних макетахisolation: isolatewill-change(так, воно може перемістити елементи у власні шари і змінити композитну поведінку)
На сайтах документації звичним винуватцем є фіксований хедер з transform для плавного скролу або мікроанімацій. Тоді ваш «глобальний оверлей» перестає бути глобальним.
Практичні поради
- Рендерте оверлей і шторку в корені документа (портал у
document.body), а не всередині трансформованого контейнера. - Використовуйте окремий верхній шар для UI: наприклад, створіть
#ui-layerяк останній дочірній елементbody. Уникайте вкладення всередині основних обгорток макету. - Не покладайтесь на магічні числа z-index. Встановіть невелику систему: header 10, drawer 100, modal 1000. І дотримуйтесь її.
Оверлей має надійно блокувати pointer events
Використовуйте оверлей як реальний елемент, що захоплює події вказівника. «Затемнюйте фон» шляхом встановлення фонової кольору оверлею, а не застосування opacity до всієї сторінки. Якщо ви фейдите сторінку, ви також затаюєте текст, і скрінрідери цього не помітять, але люди помітять.
Блокування прокрутки: фіксація body, iOS Safari і пастка position: fixed
Блокування прокрутки — це місце, де більшість шторок помирає. Десктопні браузери часто пробачають недбалість. Мобільні запам’ятовують і карають вас резиновою прокруткою, тремтінням контенту та проклятим «сторінка повертається вгору при закритті».
Чого ви намагаєтеся досягти
Коли шторка відкрита:
- Документ під оверлеєм не має прокручуватися.
- Сама панель шторки може прокручуватися (списки навігації довгі).
- Дотична прокрутка в середині шторки не повинна «ланцюжитися» в прокрутку body.
- При закритті поверніть body точно до попередньої позиції прокрутки.
Надійний патерн: заблокувати body фіксованою позицією й збереженою scrollY
Це патерн, що працює в більшості версій mobile Safari:
- Зберіть
scrollYпри відкритті. - Встановіть для
bodyposition: fixed,top: -scrollY,left: 0,right: 0,width: 100%. - При закритті видаліть ці стилі і відновіть прокрутку через
window.scrollTo(0, savedScrollY).
Так, це виглядає як хак. Це хак. Але це стабільний хак, за який ми платимо.
Чому не просто overflow: hidden на body?
Бо mobile Safari непослідовний щодо прокрутки body: іноді контейнер прокрутки — це html, іноді — body, іноді все залежить від встановленої висоти. Ви можете зробити overflow: hidden працюючим на вашому пристрої і все одно випустити зламаний досвід для інших.
Зупиніть ланцюження прокрутки за допомогою overscroll behavior
Для внутрішнього контейнера прокрутки шторки:
- Використовуйте
overscroll-behavior: contain, де підтримується, щоб запобігти ланцюжку прокрутки в body. - На iOS також розгляньте
-webkit-overflow-scrolling: touchдля плавного скролу, але тестуйте — деякі комбінації з фіксованим body можуть все одно смикати.
Висота viewport: припиніть використовувати сире 100vh на мобайлі
Віддавайте перевагу динамічним одиницям viewport (dvh), де доступно, і надавайте резервні варіанти. Для висоти шторки часто використовують height: 100dvh з резервом на 100vh. Якщо ваш CSS-пайплайн це підтримує, можна прогресивно покращувати.
Другий жарт (і останній, заради політики та власної гідності): Mobile Safari — єдине місце, де «працює на моєму телефоні» не є заспокійливою заявою.
Керування фокусом і доступність: трап, відновлення та вихід
Користувачі документації включають тих, хто користується клавіатурою, скрінрідерами і людей, які просто не хочуть весь день торкатися екрану. Якщо шторка ламає фокус — вона ламає сайт для них.
Мінімальний контракт доступності
- Кнопка перемикання має доступну назву (наприклад, «Open navigation»).
- Кнопка відображає стан за допомогою
aria-expanded="true/false". - Шторка має семантичний контейнер: зазвичай
navабоdivз доступною міткою. - При відкритті фокус переміщується у шторку (зазвичай на перший фокусований елемент або кнопку закриття).
- Фокус затримується в шторці, поки вона відкрита.
- При закритті фокус повертається до кнопки перемикання.
- Esc закриває шторку.
Використовуйте inert, де можете
Встановлення решти сторінки як inert під час відкриття шторки — найчистіший спосіб запобігти фоновому фокусу й клікам. Це не магія: вам все одно потрібно керувати блокуванням прокрутки і відображенням видимого оверлею для натисків. Але inert сильно зменшує дивні кейси з фокусом.
Якщо не можна покладатися на inert, можна апроксимувати його через:
- Додавання
aria-hidden="true"до основного контенту, поки шторка відкрита (але будьте обережні: надмірне приховування може позбавити корисного контексту для засобів допомоги). - Використання реалізації focus trap, що циклічно тримає фокус в шторці.
- Вимкнення pointer events для основного контенту (
pointer-events: none) під час активного оверлею.
Фокус-трапи: три правила, що запобігають 90% багів
- Трап має активуватися після того, як шторка в DOM і видима. Інакше перший Tab може вирватися назовні.
- Трап має деактивуватися навіть якщо шторка закрита через маршрутизацію. Зміни маршруту — місце, де трапи гниють.
- Завжди відновлюйте фокус. Користувачі на нього розраховують. Також це гарний індикатор: чи справді виконалася логіка закриття?
Зменшена анімація — це не «прикраса»
Якщо користувач віддає перевагу зменшеній анімації, не рухайте повноекранну панель по всьому вікну. Перейдіть на майже миттєвий трансформ або фейд. Анімація шторки декоративна; навігація — це продукт.
Маршрутизація, гідрація і життєвий цикл: закривати шторку в потрібний момент
Сайти документації часто працюють як SPA або гібридні додатки. Це змінює спосіб, як шторки ламаються:
- Проміжок гідрації: HTML відрендерений, користувач швидко натискає меню, JS-обробники ще не підключені. Результат: нічого не відбувається або сторінка прокручується.
- Переходи маршрутів: клік навігації змінює контент сторінки, але залишає глобальний UI в неконсистентному стані (шторка відкрита, body заблокований).
- Відновлення прокрутки: фреймворки іноді відновлюють прокрутку при зміні маршруту, конкуруючи з вашою логікою відновлення блокування прокрутки.
Правила, що збережуть вас у здоровому глузді
- Закривайте при зміні маршруту, підписавшись на події роутера. Це обов’язково.
- Закривайте при зміні брейкпоінта: при переході з мобільного на десктопний макет примусово закривайте і розблокуйте прокрутку.
- Будьте захисними при розмонтуванні: якщо компонент розмонтовується під час відкриття, очищення має все одно відновити стилі body і стан фокусу.
Гідрація: уникайте синдрому «мертвої кнопки»
Для статично відрендерених документів розгляньте:
- Рендерити кнопку перемикання як реальний
<button>з мінімальним inline-скриптом для відкриття/закриття ще до повної гідрації (якщо платформа дозволяє). - Або затримати показ кнопки до готовності JS (менш ідеально; це коротко приховує навігацію).
Думка: якщо документація — ваш фронтдвер продукту, не відправляйте шторку, що залежить від 300KB гідраційного пакета, щоб працювати.
Продуктивність: що не анімувати та чому blur — це витрата
Шторки — це пастки продуктивності, бо виглядають просто. Насправді вони зачіпають лейаут, композитинг, скролінг і обробку подій одночасно.
Анімувати трансформи, а не лейаут
Використовуйте transform: translateX() для панелі, а не left або width. Анімації, що торкаються лейаута, можуть спричиняти багато reflow і repaint по всій сторінці — особливо болісно на довгих документах з блоками коду та підсвіткою синтаксису.
Backdrop blur: розглядайте як залежність продакшну
backdrop-filter: blur() виглядає чудово. Воно також змушує браузер постійно перерендирувати те, що під оверлеєм. На деяких пристроях це не «трохи повільніше». Це «кадри падають нижче 30fps і UI здається зламаним».
Якщо ви мусите використовувати blur:
- Робіть його умовним залежно від налаштувань зменшеної прозорості/анімації.
- Обмежте радіус blur.
- Тестуйте на середньому Android і старих iPhone, а не лише на вашому ноуті.
Слухачі подій: не протікайтесь і не дублюйте
Код шторки часто додає keydown і touchmove слухачі. Якщо ви додаєте їх при кожному відкритті і забуваєте прибрати — отримаєте множинні виклики. Симптоми: подвійне закриття, смикання і дивні сплески CPU. В продакшні це виглядає як «сайт документації гіршає, чим довше ним користуватись».
Три корпоративні історії з реальних проєктів
Інцидент: неправильне припущення («overflow: hidden достатньо»)
Команда випустила оновлену документацію з приємною висувною шторкою. Десктоп і Android Chrome були в порядку. Команда потішилася і пішла далі — саме так запрошується хаос.
За день почали надходити звернення до підтримки: «Меню відкривається, але сторінка позаду прокручується. Потім при закритті меню мене кудись перескакує». Звернення були переважно від користувачів iPhone, але не всі. Першим припущенням команди був z-index, бо це загальний баг шторок.
Насправді корінь був у одній стрічці: body { overflow: hidden; }, що перемикали при відкритті. Воно «працювало» на їхніх тестових пристроях і провалювалося на інших через відмінності в контейнерах прокрутки та динаміку адресного рядка. Деякі користувачі мали відкрите меню, а сторінка позаду «резиново» прокручувалась; інші бачили, що body розблоковується в дивні моменти, бо маршрут змінився і компонент розмонтувався без очищення.
Виправлення вимагало двох змін: (1) перейти на фіксований body з saved scrollY, і (2) реалізувати глобальне очищення на розмонтування і зміну маршруту. Веселий момент: коли вони виправили блокування прокрутки, виявився прихований баг з фокусом, бо фонові елементи все ще були табулюваними. Шторка покладалася на «користувачі торкаються екрану», що не є стратегією для фокусу.
Інцидент не був катастрофічним, але завдав репутаційної шкоди. До документації ходять люди, коли вони вже роздратовані. Зламана мобільна навігація — це як замкнути аварійний вихід і здивуватися.
Оптимізація, що зламала все: «GPU-активувати все»
Інша організація хотіла, щоб документація «відчувалася нативною». Хтось додав transform: translateZ(0) і will-change: transform до хедера і кількох обгорток для «покращення скролу». Оверлей і панель були вкладені в одну з цих обгорток, бо так склалося дерево компонентів.
У щасливому шляху вигляд було гладким. Потім користувачі повідомили, що натискання поза шторкою іноді не закривало її. Деякі натискання проходили крізь оверлей до посилань позаду. На деяких сторінках оверлей взагалі не покривав хедер. Також зламалося порівняння скріншотів у тестах, бо композиція пікселів відрізнялась між запуском і запуском.
Корінь: «оптимізація» створила новий стекінг-контекст і поведінку композитного шару, що змінила порядок хіт-тестів. Оверлей мав високий z-index у своєму контексті, але сам контекст лежав під окремим контекстом фіксованого хедера. В деяких браузерах композитор вважав оверлей візуально зверху, але все одно дозволяв подіям вказівника проходити до хедера. Це особливий різновид прокляття.
Відкат прибрав більшість will-change підказок. Реальне покращення продуктивності прийшло пізніше, коли зменшили DOM вага в навігації і відклали підсвітку синтаксису до idle. Анімація шторки сама ніколи не була вузьким місцем; проблемою була сторінка.
Нудна але правильна практика, що врятувала день: контракт очищення
Третя команда мала суворе правило: будь-який компонент, що мутує глобальний стан, має надавати явний шлях очищення, покритий автоматизацією. Шторка мутувала глобальний стан: стилі body для блокування прокрутки, inert/aria-hidden для основного контенту і document-level обробники клавіш.
Вони написали невеликий модуль «UI lock», що володів цими змінами. Компоненти могли запитати блокування з токеном; звільнення токена відновлювало попередній стан лише коли останній токен звільнявся. Це не було привабливо. Але воно витримувало ре-ентрансі і розмонтування під час зміни маршруту.
Місяці потому апгрейд роутера змінив таймінги подій переходу. В інших командах шторки почали залишати body заблокованим. У цієї команди контракт очищення все одно спрацював, бо був прив’язаний і до розмонтування, і до подій завершення маршруту, а також їхні E2E-тести стверджували, що після навігації body не має fixed-позиціювання і фокус опущений у контент.
Ніхто не писав торжественного емейла. Звісно ні. Надійний UI — як надійне сховище: якщо люди помічають його, щось уже пішло не так.
Плейбук швидкої діагностики
Коли шторка зламанa в продакшні, потрібен швидкий шлях до вузького місця. Ось порядок, що мінімізує витрату часу.
1) Це проблема стекінгу / оверлею?
- Перевірка: Чи візуально оверлей покриває все? Чи перехоплює він тапи?
- Сигнал: Якщо натискання проходять крізь, або хедер сидить над оверлеєм — підозрюйте стекінг-контексти і розміщення порталу.
- Термінове рішення: Перенесіть оверлей/панель у кореневий портал; видаліть transform/filter з предків; стандартизуйте шари z-index.
2) Це проблема блокування прокрутки?
- Перевірка: При відкритій шторці можна прокручувати сторінку позаду? При закритті прокрутка перескакує?
- Сигнал: Стрибок на верх при закритті — класичний «overflow hidden» або невідповідність блокування body.
- Термінове рішення: Перейдіть на fixed-body lock з збереженою позицією прокрутки; забезпечте очищення на кожному шляху закриття.
3) Це проблема фокусу / клавіатури?
- Перевірка: При роботі з клавіатурою Tab виривається? Esc закриває? Фокус повертається до перемикача після закриття?
- Сигнал: Втеча фокусу зазвичай означає, що трап не активований вчасно або фон не інертний.
- Термінове рішення: Додайте inert або aria-hidden; реалізуйте надійний focus trap; відновлюйте фокус явно.
4) Це продуктивність / дженк?
- Перевірка: Відкриття шторки падає кадри або зависає? CPU стрибає?
- Сигнал: Бекдроп-блюр, великий DOM навігації, примусове перерахування лейауту або дубльовані слухачі подій.
- Термінове рішення: Видаліть blur, анімувати лише трансформи, зменшіть DOM, перевірте слухачі подій і reflow.
Поширені помилки: симптом → корінь → виправлення
Оверлей не покриває хедер
Симптом: Фіксований хедер залишається клікабельним/видимим поверх затемненого фону.
Корінь: Оверлей знаходиться всередині меншого стекінг-контексту; хедер створив окремий стекінг-контекст через transform/filter або правила z-index.
Виправлення: Рендерте оверлей/панель через портал в кінець body; видаліть CSS, що створює стекінг-контексти, з обгорток макету; визначте шкалу z-index.
Натискання «просочуються» крізь оверлей
Симптом: Клік поза шторкою активує посилання позаду неї.
Корінь: Оверлей має pointer-events: none, або він не покриває viewport, або композитинг/хіт-тест mismatch через трансформи.
Виправлення: Переконайтесь, що оверлей має position: fixed; inset: 0; з включеними pointer events; уникайте вкладення в трансформовані предки.
Фон прокручується під відкритою шторкою
Симптом: Можна прокрутити сторінку під час відкритої шторки; з’являється «резиновий» ефект.
Корінь: Використано тільки overflow: hidden або заблоковано невірний елемент; ланцюження прокрутки з контейнера шторки в body.
Виправлення: Використовуйте фіксований body з збереженою позицією прокрутки; встановіть overscroll-behavior: contain на зоні прокрутки шторки.
Закриття шторки перекидає сторінку вгору
Симптом: При закритті шторки позиція прокрутки скидається або зсувається.
Корінь: Body був встановлений як position: fixed без відновлення прокрутки; або відновлення прокрутки фреймворком конкурує з вашою логікою; або ви змінили висоту html/body.
Виправлення: Зберігайте scrollY при відкритті; встановлюйте top: -scrollY; при закритті видаляйте стилі і викликайте window.scrollTo(0, savedY). Інтегруйте з відновленням прокрутки роутера.
Фокус клавіатури виривається в контент позаду
Симптом: Натискаєте Tab і фокус переходить до посилань у основному контенті, а не в шторці.
Корінь: Немає фокус-трапу, або трап активується занадто пізно, або фон залишається фокусованим.
Виправлення: Використайте фокус-трап і активуйте його негайно при відкритті; встановіть inert на основний контент; відновлюйте фокус при закритті.
Esc закриває шторку іноді, але не завжди
Симптом: Esc працює один раз, потім перестає, або працює лише на певних сторінках.
Корінь: Слухач keydown прикріплений до компонента, що розмонтовується; дубльовані слухачі; фокус у iframe/блокові коду перехоплює клавіші.
Виправлення: Прикріпляйте keydown на рівні документу під час відкриття; забезпечуйте очищення; ігноруйте Esc під час композиції IME; обробляйте фокус у вкладених віджетах.
Шторка відкривається, але скрінрідер каже нісенітницю
Симптом: Скрінрідер не оголошує навігацію або читає фоновий контент під час відкритої шторки.
Корінь: Відсутні мітки, неправильне використання aria-hidden, відсутнє управління фокусом або фон не інертний.
Виправлення: Промаркуйте навігацію; перемістіть фокус у неї при відкритті; правильно застосуйте inert або приховання фону; переконайтесь, що кнопка закриття доступна й має ім’я.
Відкриття шторки лагає
Симптом: Падають кадри, лаг на дотик, затримка старту анімації.
Корінь: Анімація лейаут-параметрів; важкий бекдроп-блюр; примусові синхронні читання/записи через JS; великий DOM у навігації.
Виправлення: Анімуйте трансформи; видаліть blur або зменшіть радіус; групуйте читання/записи DOM; віртуалізуйте або згорніть секції навігації.
Завдання для продакшну з командами: перевірити, виміряти, вирішити
UI-баги виглядають як «фронтенд-проблеми», але їх діагностика виграє від тієї ж дисципліни, що й для зберігання та надійності: спочатку виміряйте, потім змінюйте, потім перевірте. Нижче практичні завдання, які можна виконати з shell під час відтворення проблем у staging або продакшн-подібному середовищі.
Припущення: у вас є доступ до тестового хоста, що запускає сайт документації, логів і, опційно, headless-браузера. Команди реалістичні й виконувані; адаптуйте шляхи файлів і назви сервісів.
Завдання 1: Підтвердити який HTML відправляється (SSR чи тільки клієнт)
cr0x@server:~$ curl -sS -D- https://docs.internal.example/guide/install | head -n 30
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=0, must-revalidate
x-powered-by: app
...
<!doctype html>
<html lang="en">
...
Що означає вивід: Ви перевіряєте заголовки відповіді і чи доставляється HTML. Якщо бачите в основному пустий HTML з посиланням на великий скрипт і без розмітки навігації, ваша шторка залежить від гідрації.
Рішення: Якщо шторка залежить від гідрації, пріоритетизуйте «no dead button» (вбудований мінімальний скрипт або серверний рендеринг оболонки навігації).
Завдання 2: Перевірити, що кешування не подає невідповідні версії JS/CSS
cr0x@server:~$ curl -sSI https://docs.internal.example/assets/app.css | egrep -i 'cache-control|etag|last-modified'
cache-control: public, max-age=31536000, immutable
etag: "a9f4c2-18b10"
last-modified: Tue, 10 Dec 2024 18:22:11 GMT
Що означає вивід: Довге кешування прийнятне лише якщо ассети content-hashed і HTML посилається на правильні версії.
Рішення: Якщо HTML вказує на не-hashed ассети з довгим TTL, можете отримати «drawer JS не відповідає HTML/CSS». Виправте заголовки кешування або версіонування ресурсів.
Завдання 3: Перевірити Content Security Policy, що впливає на inline-скрипти для ранньої інтерактивності шторки
cr0x@server:~$ curl -sSI https://docs.internal.example/ | egrep -i 'content-security-policy'
content-security-policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
Що означає вивід: Якщо ви плануєте використовувати маленькі inline-скрипти, CSP може їх блокувати.
Рішення: Або тримайте поведінку шторки в бандлі, або оновіть CSP з nonce-підходом. Не додавайте просто unsafe-inline.
Завдання 4: Знайти спайки помилок, пов’язаних із взаємодією зі шторкою (клієнтські помилки вжиті серверно)
cr0x@server:~$ sudo journalctl -u docs-web -S "2 hours ago" | egrep -i 'TypeError|Unhandled|focus|inert|scroll' | tail -n 20
Dec 29 12:11:04 web-01 docs-web[2410]: UnhandledRejection: TypeError: Cannot read properties of null (reading 'focus')
Dec 29 12:14:19 web-01 docs-web[2410]: TypeError: Failed to execute 'setAttribute' on 'HTMLElement': 'inert' is not a valid attribute name
Що означає вивід: Баги відновлення фокусу і неправильне використання inert можуть кидати виключення, які призводять до «шторка не закривається», бо очищення не виконується.
Рішення: Розглядайте це як питання надійності: додайте перевірки, забезпечте очищення в finally-блоках і робіть feature-detect для inert.
Завдання 5: Підтвердити, що елемент оверлею існує і не видаляється/мінімізується некоректно
cr0x@server:~$ curl -sS https://docs.internal.example/ | grep -n 'data-testid="nav-overlay"' | head
184: <div data-testid="nav-overlay" class="overlay" hidden></div>
Що означає вивід: Ви перевіряєте, що оверлей дійсно в DOM як відправлений SSR або присутній у шаблоні.
Рішення: Якщо його немає — дебаг CSS не допоможе. Виправляйте рендеринг/шаблонізацію спочатку.
Завдання 6: Перевірити несподівано великі JS-бандли, що затримують гідрацію
cr0x@server:~$ curl -sSI https://docs.internal.example/assets/app.js | egrep -i 'content-length|content-encoding'
content-encoding: br
content-length: 612341
Що означає вивід: ~600KB brotli-бандл може бути важким на мобайлі через парс/компіляцію.
Рішення: Якщо функціонал шторки чекає цей бандл, розділяйте критичний UI, відкладайте неважливі скрипти і не робіть навігацію заручницею аналітики.
Завдання 7: Підтвердити серверну компресію для CSS/JS (повільна передача = довше «мертва кнопка»)
cr0x@server:~$ curl -sSI -H 'Accept-Encoding: gzip, br' https://docs.internal.example/assets/app.js | egrep -i 'content-encoding|vary'
vary: Accept-Encoding
content-encoding: br
Що означає вивід: Компресія увімкнена і варіюється відповідно до запиту.
Рішення: Якщо компресії немає — включіть її до виправлень до редизайну шторки. Латентність — це прапорець функціоналу, який ви забули додати.
Завдання 8: Перевірити, що маршрути шторки закривають її (серверні логи для SPA не допомагають; використовуйте синтетичні перевірки)
cr0x@server:~$ node -e "console.log('Run an E2E check here with Playwright/Cypress in CI; server logs cannot see client route changes.')"
Run an E2E check here with Playwright/Cypress in CI; server logs cannot see client route changes.
Що означає вивід: Пряме нагадування: ви не можете діагностувати клієнтські state-баги тільки з серверних логів.
Рішення: Додайте синтетичну браузерну перевірку, що відкриває шторку, клікає навігацію, перевіряє, що body розблокований і фокус опинився в контенті.
Завдання 9: Подивитися на NGINX misconfiguration, що ламає range-запити (псує перформанс на мобайлі)
cr0x@server:~$ curl -sSI https://docs.internal.example/assets/app.js | egrep -i 'accept-ranges'
accept-ranges: bytes
Що означає вивід: Range-запити можуть допомогти деяким клієнтам і CDN. Не баг шторки, але впливає на time-to-interactive, що впливає на відчуття відповіді шторки.
Рішення: Якщо відсутній — перевірте конфігурацію роздачі статичних файлів.
Завдання 10: Підтвердити час відповіді і tail latency на сторінках з важкою навігацією
cr0x@server:~$ curl -sS -w "ttfb=%{time_starttransfer} total=%{time_total}\n" -o /dev/null https://docs.internal.example/guide/reference
ttfb=0.142315 total=0.211904
Що означає вивід: Якщо TTFB високий — HTML приходить пізно. Якщо total високий — мережа повільна. Обидва фактори роблять UI «мертвим».
Рішення: Якщо TTFB стрибає — виправляйте кешування бекенду/рендер. Якщо мережа повільна — поліпшіть CDN, компресію і стратегію ассетів.
Завдання 11: Слідкувати за тиском пам’яті на сервері, що спричиняє повільні відповіді (проблема не завжди фронтенд)
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 31Gi 26Gi 1.2Gi 352Mi 3.9Gi 4.3Gi
Swap: 2.0Gi 1.8Gi 256Mi
Що означає вивід: Низька доступна пам’ять і використання swap можуть призвести до латентних сплесків при віддачі HTML/JS, затримуючи інтерактивність.
Рішення: Якщо є свопінг — виправляйте тиск пам’яті перед тим, як звинувачувати CSS. Користувачів не хвилює, де саме баг.
Завдання 12: Виявити перевантаження CPU під час пікового навантаження на документацію
cr0x@server:~$ uptime
12:29:44 up 18 days, 4:12, 2 users, load average: 6.21, 6.02, 5.88
Що означає вивід: Високе середнє навантаження відносно ядер CPU може спричинити повільний HTML і затримане доставляння JS.
Рішення: Якщо навантаження постійно високе — масштабування, кешування або зменшення затрат на рендер. Потім перевіряйте «реактивність» шторки знову.
Завдання 13: Перевірити розміри статичних ресурсів на диску, щоб виявити випадкові debug-білди
cr0x@server:~$ ls -lh /var/www/docs/assets | egrep 'app\.(js|css)' | head -n 5
-rw-r--r-- 1 www-data www-data 5.9M Dec 10 18:22 app.js
-rw-r--r-- 1 www-data www-data 412K Dec 10 18:22 app.css
Що означає вивід: Якщо app.js багатомегабайтний незжатий — можливо ви шлете сорсмапи або дев-білд у продакшн.
Рішення: Виправте пайплайн збірки. Баги шторки множаться, коли клієнт бореться з вашим JavaScript-романом.
Завдання 14: Перевірити, чи кешуються сторінки помилок або редиректи (шторка «ламається», бо сторінка не та сама)
cr0x@server:~$ curl -sSI https://docs.internal.example/guide/install | egrep -i 'http/|location:|cache-control:'
HTTP/2 200
cache-control: public, max-age=0, must-revalidate
Що означає вивід: Якщо ви отримали редирект або дивні заголовки кешування, клієнти можуть бачити застарілий або частковий HTML (з відсутніми скриптами навігації).
Рішення: Переконайтесь у коректному кешуванні HTML і правильній обробці редиректів. Відсутність JS шторки через показ інтерстиціалу логіна — все одно відмова шторки.
Завдання 15: Перевірити, чи service worker (якщо є) не віддає застарілий shell HTML
cr0x@server:~$ grep -R "workbox" -n /var/www/docs/ | head
/var/www/docs/sw.js:12:importScripts('workbox-*.js');
Що означає вивід: Service worker може агресивно кешувати app shell і подавати невідповідний HTML/JS після релізу.
Рішення: Якщо використовуєте SW, реалізуйте версіонування і інвалідизацію кешу. Інакше будете дебажити «випадкові» регресії шторки від застарілих клієнтів.
Завдання 16: Пошук дубльованого прикріплення обробників подій у збірці (швидкий grep)
cr0x@server:~$ grep -R "addEventListener(\"keydown\"" -n /var/www/docs/assets/app.js | head
12877:document.addEventListener("keydown",u)
Що означає вивід: Не доказ витоків, але показує, де прив’язуються обробники клавіш. Якщо додавати слухачі при кожному відкритті без видалення — отримаєте множинні виклики під час виконання.
Рішення: Аудит життєвого циклу і очищення. Додайте інструментування, якщо потрібно. Витоки UI-подій — кузина витоків файлових дескрипторів: ігноруються, доки не вкусить.
Чеклисти / покроковий план
Покроковий план реалізації (нудна версія, що працює)
- Розмістіть оверлей і шторку в топ-рівневому UI-шарі, доданому до
body(портал). Уникайте трансформованих предків. - Створіть шкалу z-index і дотримуйтеся її під час code review. Якщо хтось додає
z-index: 999999, попросіть пояснити рішення. - Реалізуйте блокування прокрутки через збережений scrollY + фіксований body. Додайте очищення на кожному шляху виходу.
- Зробіть панель шторки власним контейнером прокрутки з containment для overscroll.
- Реалізуйте управління фокусом: збережіть активний елемент, перемістіть фокус у шторку при відкритті, затримуйте фокус, відновіть при закритті.
- Реалізуйте механізми закриття: клік по оверлею, кнопка закриття, Esc, зміна маршруту, зміна брейкпоінта.
- Додайте підтримку reduced motion, щоб уникнути важких переходів.
- Тестуйте на iOS Safari з довгими сторінками і глибокою прокруткою. Не погоджуйтеся з «працює в Chrome-емуляторі».
- Додайте E2E-перевірки, що верифікують відновлення прокрутки і фокусу після навігації.
- Інструментуйте помилки з шляхів фокусу/прокрутки; трактуйте їх як проблеми доступності/доступності сервісу.
Чеклист перед злиттям (що вимагати під час рев’ю)
- Оверлей має
position: fixedі покривати весь вікно перегляду. - Немає оверлею/шторки всередині трансформованої обгортки макету.
- Блокування body зберігає та відновлює позицію прокрутки детерміновано.
- Всі шляхи закриття викликають ту саму функцію очищення.
- Фокус відновлюється до кнопки перемикання після закриття.
- Шторка має кнопку закриття, доступну клавіатурою.
- Esc закриває; Tab не виривається.
- Повага до reduced motion.
- Зміна маршруту закриває шторку і розблоковує прокрутку.
- Не використовувати blur/backdrop-filter без підтвердженого тестування продуктивності.
Чеклист релізу (реальність продакшну)
- Перевірити заголовки кешування: HTML не надто кешується; ассети незмінні з хешами.
- Переконатися, що бюджети розмірів бандлів не регресували і не впливають на time-to-interactive.
- Запустити синтетичний мобільний тест навігації у CI і після деплою.
- Моніторити інжест клієнтських помилок для помилок фокусу/прокрутки.
- Мати шлях відкату, що також інвалідовує кеш service worker, якщо він використовується.
Питання й відповіді
1) Чи має навігаційна шторка документації бути <dialog>?
Зазвичай ні. Ставтеся до неї за поведінкою як до модального вікна, але семантично це навігація. Використовуйте контейнер nav, оверлей і фокус-трап. <dialog> може працювати, але приносить свої особливості і обмеження стилізації.
2) Чи безпечно використовувати inert?
Зараз його можна широко використовувати, але все одно робіть feature-detect і тестуйте. Якщо не можна покладатися на нього в усіх важливих середовищах, використайте fallback: focus trap плюс обережне управління aria. Не випускайте щось, що блокує кліки, але при цьому дозволяє фоновий фокус.
3) Чому моя сторінка стрибає при закритті шторки?
Бо ваше блокування прокрутки не відновило позицію прокрутки правильно, або механізм відновлення прокрутки фреймворку конкурує з вашою логікою. Зберігайте scrollY при відкритті, блокуйте body фіксованою позицією з top: -scrollY, потім відновлюйте scrollY при закритті і координуйтеся з поведінкою роутера.
4) Чи справді потрібно робити фокус-трап для навігаційної шторки?
Так, якщо шторка веде себе як оверлей, що блокує сторінку. Інакше клавіатурні користувачі можуть потрапити в невидимий або закритий контент позаду шторки. Це не просто «трохи дратує» — це поламаний кейс.
5) Чому оверлей сидить під деякими елементами, навіть з великим z-index?
Тому що z-index обмежений стекінг-контекстами. Якщо оверлей живе всередині стекінг-контексту, що лежить під стекінг-контекстом хедера, він не зможе «виперти» його. Виправте розміщення в DOM (портал) і приберіть тригери стекінг-контексту з предків.
6) Чи має шторка закриватися при кліку на навігаційне посилання?
Так. Завжди. Закривайте негайно при кліку (оптимістично), потім нехай відбудеться маршрутизація. Також закривайте при подіях зміни маршруту на випадок програматичного переходу, натиску back/forward або зміни hash.
7) Чи варта backdrop blur витрат?
Тільки якщо ви можете довести, що він не вбиває продуктивність на репрезентативних пристроях. Blur постійно коштує під час анімації і коли шторка відкрита. Простий напівпрозорий оверлей дешевший і передбачуваніший.
8) Як працювати з вкладеними деревами навігації без перетворення шторки на неюзабельну?
Використовуйте поступове розкриття: зазвичай секції згорнуті за замовчуванням, зберігайте розгорнутий стан користувача в сесії і тримайте шлях активної сторінки розгорнутим. І тримайте DOM навігації легшим за ваше его — глибокі дерева дорогі для рендерингу і скролу.
9) А як щодо жестів свайп для відкриття/закриття шторки?
Будьте обережні. Жести конфліктують з навігацією браузера і скролом. Якщо робите свайп, робіть його опціональним і другорядним до явних контролів. Ваш основний обов’язок — «відкриватися надійно», не «відчуватися як демо нативного застосунку».
10) Як правильно це тестувати?
Запустіть E2E-тести, що стверджують: оверлей покриває viewport, фон не прокручується, фокус переходить у шторку, Tab лишається всередині, Esc закриває, фокус повертається до перемикача, і зміна маршруту розблоковує прокрутку. Потім вручну протестуйте на iOS Safari з довгою сторінкою, прокрученій до глибокої позиції.
Висновок: наступні кроки, що витримують продакшн
Мобільна шторка навігації документації — це не дизайн-фішка. Це інфраструктура. Вона вирішує, чи зможуть користувачі вийти зі сторінки, знайти потрібний гайд і зберегти свою позицію під час налагодження о 2 ранку — саме тоді ви й будете дебажити шторку, якщо випустите її халтурно.
Робіть наступне:
- Перенесіть оверлей/шторку в топ-рівневий портал і стандартизуйте z-index.
- Реалізуйте фіксоване блокування body з збереженою позицією прокрутки і надійним очищенням.
- Додайте
inert(або еквівалент) і реальний фокус-трап; відновлюйте фокус при закритті. - Закривайте при зміні маршруту і при зміні брейкпоінта — завжди.
- Запустіть один синтетичний E2E-тест, що голосно падає, якщо body залишився заблокованим або фокус вирвався.
Випустіть нудну шторку. Ваші користувачі ніколи вам не подякують, і це буде доказом того, що все зроблено правильно.