Підказки в UI — це еквівалент фрази «лишенька невелика зміна». Додаєш значок із питанням, підвішуєш текст при наведенні, деплоїш — і продакшен швидко вчить, що ти забув: скролювані контейнери, обрізаний overflow, торкання на мобайлі, пекло z-index та користувачі клавіатури, які не можуть навести курсор.
Це прагматичний шлях: зробити підказки без бібліотеки, але з рівнем ретельності, як у бібліотеки. Якщо ваша підказка переживе липкий хедер, трансформований батько, модал, масштаб 200% і скрінрідер — вона переживе й наступний редизайн.
Що таке підказка (і чим вона не є)
Підказка — це допоміжна інформація, прив’язана до керуючого елемента або inline-елемента. Вона має пояснювати, а не нести критичний сенс. Якщо приховання підказки робить інтерфейс непридатним, ви не зробили підказку — ви зробили зламну мітку форми.
Підказки також не є поповерами, діалогами або тостами:
- Підказка: невелика, ефемерна, прикріплена до тригера, зазвичай без інтерактивного контенту.
- Поповер: теж прикріплений, але може містити інтерактивні елементи (посилання, кнопки, поля форми). Потребує багатшої обробки клавіатури.
- Діалог: модальний або немодальний, захоплює фокус, часто має backdrop і має бути закривним через клавіатуру.
Визначте це на початку, бо від цього залежить усе: ARIA-ролі, поведінка фокусу, закриття і чи можна використовувати тригери тільки на ховер.
Думка: Якщо вміст містить посилання, чекбокс або кнопку «Копіювати», припиніть називати це підказкою. Ви будуєте поповер; ставтесь до нього відповідно.
Факти та історія, які варто знати
- Факт 1: HTML-атрибут
titleбув оригінальною «безкоштовною підказкою», але він непослідовний між браузерами, важко стилізується і ненадійний для доступності. - Факт 2: Підказки — один із найстаріших UI-патернів, запозичених з десктопних GUI (згадайте класичні Windows help balloons), потім незграбно пересаджений у веб.
- Факт 3: Ранні веб-підказки часто використовували
onmouseoverіdocument.write. Ми пережили ту епоху. Ледь. - Факт 4: Проблема з z-index старша за багато фронтенд-фреймворків; контексти стекування підстерігали команди задовго до того, як «компонент» став професією.
- Факт 5: ARIA-гайди для підказок дозріли пізніше, ніж для діалогів і кнопок, тому в продакшені досі бачите невідповідні патерни.
- Факт 6: Сенсорні інтерфейси змусили підказки дорослішати: hover не існує на більшості телефонів, тож ваш «простіший hover-підказ» стає архітектурним рішенням.
- Факт 7: Перехід до патернів «portal to body» виник через реальні невдачі розкладки: обрізання overflow, трансформи і вкладені скрол-пейн робили inline-підказки крихкими.
- Факт 8: Сучасні браузери додали примітиви, які допомагають (наприклад,
ResizeObserver), але водночас додали нові пастки (наприклад, «contain» і поширені трансформи).
Незмінні вимоги для продакшен-підказок
Перед кодом випишіть, що означає «працює». Ось планка, якої я дотримуюсь у продакшені:
1) Підказка має бути помітною без hover
Користувачі клавіатури мають викликати підказки при фокусі. Зазвичай це означає, що тригер фокусований (<button>, <a> або tabindex="0" як крайній варіант).
2) Підказка не має захоплювати фокус
Підказки — неінтерактивні. Фокус лишається на тригері. Якщо підказка потребує взаємодії, оновіть її до поповера з планом управління фокусом.
3) Вона не має викликати зсуви макета
Ніякого штовхання контенту. Підказки повинні бути накладними оверлеями. CLS — не лише для маркетингових сторінок; внутрішні дашборди теж отримують осуд — від людей, що сердяться.
4) Вона має переживати скрол, зум і зміну розміру
Не «трохи». Не «переважно». Користувачі скролять, існують трекпади, а масштабування поширене у корпоративному середовищі.
5) Вона не має обрізатися
Контейнери з overflow-hidden, трансформовані предки та скрол-пейн — місця, куди наївні підказки йдуть вмирати.
6) Вона не має закривати те, чим ви намагаєтесь користуватися
Підказки, що перекривають свій тригер, створюють цикли мерехтіння і лють користувачів. Потрібен розумний відступ, обробка pointer-events і затримка закриття.
7) Вона має бути тестованою
Якщо підказку неможливо надійно вибрати в тестах, її «поправить» хтось із молотком. Забезпечте стійкі атрибути (наприклад, data-tooltip-id) і детерміністичну поведінку.
Жарт №1: Підказка, що блокує кнопку, як знак безпеки, приварений на аварійний вихід. Технічно є, практично жорстоко.
Архітектура: inline vs портал, і чому «position: absolute» — недостатньо
Є два розумні підходи, коли ви відмовляєтесь від бібліотек:
A) Inline-підказка (той самий DOM-піддерево)
Ви рендерите підказку біля тригера і позиціонуєте за допомогою CSS (position: absolute) всередині відносно позиціонованого контейнера.
Плюси: просто, менше DOM-операцій, передбачувана CSS-логіка, якщо контейнер стабільний.
Мінуси: обрізається через overflow: hidden/auto; дивності зі стекуванням; вкладені трансформи можуть робити сюрпризи.
B) Порталізована підказка (рендер під document.body)
Ви рендерите підказку в топ-рівневому оверлейному шарі (зазвичай виділений <div id="overlays">) і позиціонуєте її за координатами вьюпорта. Саме так роблять дорослі бібліотеки, бо браузери й розкладка не чутливі до сентиментів.
Плюси: уникнення обрізання локальним overflow; простіше керування z-index; консистентне розміщення в модалах та шухлядах.
Мінуси: треба обчислювати координати; треба відслідковувати скрол/resize; треба обробляти containing blocks і візуальний вьюпорт на мобайлі.
Рішення: Якщо у вас є будь-які скрол-панелі або модали (вони майже завжди є), обирайте портал за замовчуванням. Inline-підказки підходять для статичних сторінок, документації та невеликих внутрішніх інструментів, де ризик обрізання низький.
Контексти стекування: чому ваш z-index «нічого не робить»
Підказки тихо провалюються, коли з’являються за іншим елементом. Звичні винуватці:
- Батько з
transform,filter,opacity < 1,containабоisolation, що створює новий контекст стекування. - Позиціоновані елементи з власними z-index шарами.
- Фіксовані хедери та липкі елементи з високим z-index.
Порталізовані підказки оминають більшість цих проблем, живучи в відомому верхньому шарі. Але навіть тоді ваш overlay root має бути вище іншої UI-оболонки.
Алгоритм позиціонування: виміряти, розмістити, перевернути, зсунути, зафіксувати
Ось надійна ментальна модель: кожне розміщення підказки — це переговори між прямокутником тригера, розміром підказки, вьюпортом і вашим улюбленим боком.
Мінімально життєздатний алгоритм
- Виміряти тригер:
targetRect = target.getBoundingClientRect(). - Виміряти підказку: відрендерити її офскрін або приховано, потім прочитати
tooltipRect. - Розмістити на обраному боці з відступом (наприклад, 8px).
- Перевернути (flip), якщо вона виходитиме за межі (наприклад, якщо top не поміщається, поставити знизу).
- Зсунути по перехресній осі, щоб тримати всередині вьюпорта (наприклад, підсунути вліво/вправо).
- Зафіксувати кінцеві координати, щоб залишатися видимим, з невеликим відступом від країв.
Ось і все. Все інше — це багфікси в старому плащі.
Врахування вьюпорту: layout viewport vs visual viewport
Мобільні браузери ускладнюють поняття «вьюпорт». Коли з’являється екранна клавіатура або сторінка збільшена, visual viewport може відрізнятись від layout viewport. Якщо позиціонувати лише за innerWidth/innerHeight, підказки можуть опинитися в іншому «всесвіті».
Якщо серйозно: використовуйте window.visualViewport, коли доступно (і робіть fallback, коли ні). Враховуйте visualViewport.offsetLeft/offsetTop при обчисленні координат.
Скрол-контейнери: класична помилка
getBoundingClientRect() повертає координати відносно вьюпорта, що добре. Але якщо ви рендерите підказку всередині скрол-контейнера (inline-архітектура), треба транслювати координати в простір цього контейнера. Саме тут починаються багато «працює в мене» історій.
Чистий функціонал позиціонування без залежностей (псевдокод)
Не повна бібліотека. Лише достатньо структури, щоб ви не жалкували про свої рішення:
- Вхідні дані: rect тригера, розмір підказки, перевага розміщення, розмір вьюпорта, відступи, offset.
- Вихід: x, y, використане розміщення, зміщення стрілки.
Реалізуйте як чисту функцію. Потім юніт-тестуйте її з прямокутниками. Ви не зможете інтеграційними тестами вирішити геометрію.
Робота з трансформами та фіксованим позиціонуванням
Якщо ви порталите в body, position: fixed на підказці часто найпростіший варіант: ваші x/y — координати вьюпорта. Якщо використовуєте position: absolute, доведеться додати скрол-офсети (window.scrollX, window.scrollY) і окремо обробляти документний скрол. Fixed перемагає за здоровим глуздом.
Розміщення стрілки залежить від остаточно зсуненої позиції
Стрілка — не прикраса; це напрямний маркер. Якщо ви зсуваєте підказку вліво, щоб уникнути обрізання, стрілка має зсунутись теж. Інакше вона вказує на порожнє місце — це тонка, але реальна шкода UX.
Стрілки: трикутники, повернуті квадрати, SVG і «не обманюй користувача»
Стрілки здаються простими, доки ви їх не запустите. Ось поширені реалізації:
Варіант 1: CSS border triangle
Класика: елемент нульового розміру з бордерами, де один бордер має колір, а інші прозорі.
Плюси: працює скрізь, легкий DOM.
Мінуси: тінь важко додати красиво; анти-аліасинг може виглядати зубчасто; масштабування та тема дратують.
Варіант 2: Повернутий квадрат («ромб») з transform: rotate(45deg)
Мій дефолт: маленький квадрат, повернутий, позиціонований так, щоб половина перекривала бокс підказки.
Плюси: тіні працюють; можливі заокруглені кути; простіше темізувати.
Мінуси: потребує ретельного перекриття, щоб уникнути швів; трансформи можуть створювати контексти стекування (так, знову).
Варіант 3: Inline SVG
Плюси: чітко, легко накладати фільтри/тіні, точне вирівнювання, можна підлаштувати під дизайн-систему.
Мінуси: трохи більше розмітки; якщо недбало, виникнуть проблеми з pointer-events.
Як тримати стрілку чесною
Обчисліть offset стрілки по перехресній осі підказки:
- Якщо підказка над/під тригером: стрілка рухається вліво/вправо всередині підказки.
- Якщо зліва/праворуч: стрілка рухається вгору/вниз.
Потім зафіксуйте це зміщення теж, щоб стрілка не опинилася на заокругленому куті. Заокруглені кути + стрілка на радіусі = візуальний глітч.
ARIA-дружні патерни: клавіатура, фокус і скрінрідери
Доступність — це не моральні нотації; це стратегія запобігання інцидентам. Як тільки хтось не зможе зрозуміти іконку з помилкою через те, що підказка не оголошується, ви отримаєте тікети, ескалації та «швидке виправлення», яке зламає щось інше.
Використовуйте правильну роль і зв’язок
Для справжньої підказки (неінтерактивного контенту) використовуйте:
role="tooltip"на елементі підказки.aria-describedby="tooltip-id"на тригері.
Це повідомляє допоміжним технологіям: «ця річ описує ту річ». Просто. Не ускладнюйте.
Коли використовувати aria-label проти aria-describedby
- Використовуйте
aria-label, коли тригер не має видимого лейбу (наприклад, кнопка-іконка). Цей лейб має бути коротким і стабільним. - Використовуйте
aria-describedbyдля допоміжного тексту, пояснень помилок, підказок клавіатури або уточнень, які не повинні бути головною назвою.
Не використовуйте підказки, щоб латати відсутні лейби. Скрінрідери не повинні «відкривати» ім’я контролу.
Показувати при фокусі, ховати при blur (з затримкою)
Базові правила:
- На
focus: відкрити підказку. - На
blur: закрити підказку. - На
Escape: закрити підказку (навіть якщо це «лише підказка»).
Додайте невелику затримку закриття (приблизно 100–200ms), щоб уникнути мерехтіння, коли курсор переходить між тригером і підказкою. І так, навіть для неінтерактивних підказок користувачі таке роблять. Люди — інженери хаосу.
Не ув’язнюйте скрінрідери в світі тільки hover
Hover — не універсальна взаємодія. Багато користувачів не використовують мишу. Багато пристроїв не мають hover. Забезпечте тригери фокусом і гарантуйте, що вміст підказки оголошується через described-by зв’язок.
Тримайте вміст підказки коротким і прісним
Підказки мають бути одним-двома реченнями. Якщо потрібен абзац — ви пишете документацію. Якщо потрібна таблиця — ви робите поповер. Якщо потрібна форма — ви робите діалог. Слова мають значення; ваш UI теж.
Парафразована ідея (приписується): «Сподівання — це не стратегія», часто приписують лідерам надійності типу Gene Kranz. Плануйте доступність так само.
Модель подій: ховер, фокус, торкання і закриття
Підказки ламаються на стіках між методами введення. Ваше завдання зробити ці шви нудними.
Pointer-події: використовуйте їх, але не довіряйте сліпо
pointerenter/pointerleave можуть замінити роздільну логіку для миші і торкання, але вам все одно треба вирішити, що робити для грубих вхідних пристроїв (touch). Підказка, яка відкривається на тап, може бути прийнятною, якщо:
- Тап відкриває підказку.
- Другий тап (або тап поза) закриває її.
- Підказка не блокує наступну плановану дію.
Правила закриття, що не дратують людей
- Миша: закривати на pointerleave з невеликою затримкою; тримати відкритою, якщо вказівник переходить у підказку (опціонально).
- Клавіатура: закривати на blur або Escape.
- Торкання: закривати на тап поза; розглянути закриття при скролі.
Тремтіння і мерехтіння: проблема «я зрушив мишу на один піксель»
Цикл мерехтіння часто виникає тому, що підказка перекриває тригер. Коли вона з’являється, тригер перестає бути наведений, тому вона зникає, тригер знову наведений і так далі.
Виправлення:
- Зробіть відступ, щоб підказка не перекривала.
- Встановіть
pointer-events: noneна підказку, якщо їй не потрібна витримка наведеності. - Або реалізуйте hoverable-підказку з «інтерактивною межею» і таймерами.
Продуктивність і надійність: тремтіння, рефлоу та обробка скролу
Підказки маленькі, але можуть викликати дорогі операції у невідповідний момент: скрол, resize і pointermove. Іншими словами: постійно.
Не перепозиціонуйте при кожному mousemove
Позиціонуйте при відкритті, а потім перепозиціонуйте при:
- скролі (тротлінг)
- resize (тротлінг)
- зміні розміру тригера (ResizeObserver)
- зміні вмісту підказки (ResizeObserver або повторне вимірювання після рендера)
Якщо ви перепозиціонуєте на mousemove, ви створите микро-джанк на слабших машинах і на сторінках з важкою розкладкою.
Тротлінг через requestAnimationFrame
Для скролу/resize збирайте події і робіть одне перепозиціювання на кадр. Це тримає вас синхронно з рендер-циклом браузера і уникає зайвих обчислень розкладки.
Мінімізуйте примусовий рефлоу
getBoundingClientRect() може примусити розкладку, якщо ви читаєте після запису стилів, які впливають на розкладку. Групуйте читання перед записами. Один із простих способів: обчислити всю геометрію, а потім задати style.transform = translate3d(x,y,0) за один раз.
Віддавайте перевагу трансформам над top/left
Використовуйте transform: translate3d() для позиціонування, особливо коли підказка анімується. Це зазвичай плавніше і не викликає розкладку. «Зазвичай», бо веб — демократія крайніх випадків.
Жарт №2: Якщо ви вимірюєте розкладку в щільному циклі, Chrome залюбки покаже вам, що означає «eventually consistent» — підказка раптом з’явиться в іншому місці.
Плейбук для швидкої діагностики
Коли підказка «зламана», інженери часто годину сперечаються про CSS. Не робіть так. Виконуйте плейбук. Знайдіть вузьке місце швидко.
Спочатку: ідентифікуйте клас помилки
- Невидима: не рендериться, opacity 0, display none або за іншим шаром.
- Неправильно позиціонована: неправильні координати, простір координат, неправильні скрол-офсети.
- Обрізана: overflow hidden/auto або контейнер обрізає через контекст стекування/containment.
- Мерехтить: цикл подій (hover) або треш репозицій (scroll/resize).
- Неоголошена: скрінрідер її не читає (ARIA-налаштування або поведінка фокусу).
Друге: перевірте припущення про простір координат
- Ви використовуєте портал? Якщо так, віддавайте перевагу
position: fixedі координатам вьюпорта. - Якщо не в порталі: який offset parent? Чи є трансформований предок?
- Чи не змішуєте ви
pageX/pageYз rect, що базуються на вьюпорті?
Третє: підтвердіть стекування та обрізання
- Підказка всередині елемента з
overflow: hiddenабоoverflow: auto? - Чи якийсь предок встановлює контекст стекування (transform/opacity/filter/contain)?
- Чи дійсно z-index шару оверлею вищий за хедер/модал?
Четверте: підтвердіть ARIA-зв’язування
- Тригер має
aria-describedby, що вказує на існуючий ID? - Підказка має
role="tooltip"? - Підказка відкривається на фокус і закривається на blur/Escape?
Типові помилки: симптом → корінь → виправлення
1) Підказка з’являється в (0,0) або в лівому верхньому куті сторінки
- Симптом: підказка прикріплена до кута незалежно від тригера.
- Причина: вимірювання відбувається до того, як елемент у DOM, або
getBoundingClientRect()викликається на null/прихованому тригері; іноді застарілий ref. - Виправлення: рендерте підказку приховано (
visibility: hidden) спочатку, вимірюйте в наступному кадрі; захищайтеся від відсутніх референсів; логувати rect-и.
2) Підказка правильна до скролу контейнера
- Симптом: під час скролу підказка віддаляється від тригера.
- Причина: слухаєте лише window scroll, а не скрол-ансестора; або використовуєте absolute з неправильними скрол-офсетами.
- Виправлення: портал + fixed позиціонування; або виявити та підписатись на найближчий скрол-контейнер; перепозиціонувати на подіях скролу через rAF.
3) Підказка обрізається всередині картки/модала
- Симптом: підказка помітно обрізається по межі контейнера.
- Причина: підказка живе всередині елемента з
overflow: hidden/auto. - Виправлення: порталити в топ-рівневий оверлей; або пом’якшити overflow (рідко прийнятно); або використовувати
position: fixedз правильною стратегією стекування.
4) Підказка з’являється за хедером
- Симптом: підказка існує, але не видима поверх липких елементів.
- Причина: шар оверлею під хедером з вищим z-index; або сама підказка в нижчому контексті через предка.
- Виправлення: центральний overlay root з відомим високим z-index; уникати трансформів на предках оверлею; аудит контекстів стекування.
5) Мерехтіння при ховер
- Симптом: підказка швидко показується/ховається при рухові курсора біля тригера.
- Причина: підказка перекриває тригер і відбирає hover; негайне закриття на leave без затримки.
- Виправлення: додати offset; застосувати
pointer-events: noneдля неінтерактивної підказки; додати затримку закриття; розглянути «hover bridge» (невидимий відступ), якщо треба.
6) Скрінрідер не оголошує підказку
- Симптом: підказка видима візуально, але не проголошується при фокусі тригера.
- Причина: відсутнє
aria-describedby; ID підказки не співпадає; вміст підказки з’являється в DOM лише після події фокусу і SR не переоголошує. - Виправлення: тримайте елемент підказки в DOM (приховано), щоб described-by вказував на реальний контент; перевірте ID; відкривайте на фокус консистентно.
7) Підказка викликає джанк під час скролу
- Симптом: скрол стає ривковим, коли підказка відкрита.
- Причина: перепозиціонування на кожну подію скролу з примусовою розкладкою; важка тінь або фільтр; занадто багато спостерігачів.
- Виправлення: тротлити через rAF; зменшити дорогі CSS-ефекти; уникати filter-ефектів; перепозиціонувати тільки коли підказка відкрита.
Практичні завдання (з командами): дебажте як SRE
Ви, звісно, можете дебажити UI в браузері. Але коли баг «тільки в продакшені» і «тільки для деяких користувачів», потрібна системна спостережуваність: яка збірка, які заголовки, який CSP, які ассети, які помилки. Ці завдання — нудна мускулатура, що запобігає героїчним здогадкам.
Завдання 1: Підтвердити, який HTML відправлено (і чи існує розмітка підказки)
cr0x@server:~$ curl -sS -D- https://app.example.internal/settings | sed -n '1,40p'
HTTP/2 200
date: Mon, 29 Dec 2025 10:11:12 GMT
content-type: text/html; charset=utf-8
content-security-policy: default-src 'self'; script-src 'self'
etag: W/"a1b2c3"
...
Що означає вивід: Ви перевіряєте статус, заголовки (особливо CSP) і чи взагалі повертається HTML. CSP важливий, бо стратегії підказок часто залежать від inline-стилів або скриптів.
Рішення: Якщо CSP блокує inline-стилі/скрипти, а ваша підказка їх використовує, або рефакторити в зовнішні ресурси, або безпечно оновити CSP.
Завдання 2: Перевірити, чи присутній JS-бандл підказок і чи кешується
cr0x@server:~$ curl -sS -I https://app.example.internal/assets/tooltip.js
HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=31536000, immutable
etag: "9f8e7d6c"
Що означає вивід: Бандл існує і підходить для кешування. Якщо бачите 404 або короткі терміни кешування, це проблема деплою або інвалідації кешу.
Рішення: Якщо кешування неправильне, виправте fingerprinting ассетів або конфіг CDN перед тим, як лізти в код підказок.
Завдання 3: Перевірити, чи спайки помилок збігаються з моментами рендеру підказок
cr0x@server:~$ kubectl -n web logs deploy/frontend --since=15m | grep -E "TypeError|ReferenceError|CSP|tooltip" | tail -n 20
TypeError: Cannot read properties of null (reading 'getBoundingClientRect')
CSP: Refused to apply inline style because it violates the following Content Security Policy directive...
Що означає вивід: Null-reference натякає, що елемент тригера не існує під час вимірювання (гонка), або селектор у проді неправильний. CSP-помилка каже, що стилі блокуються.
Рішення: Якщо бачите порушення CSP — зупиняйтесь. Виправте політику або реалізацію. Якщо null rect — додайте перевірки і відкладіть вимірювання.
Завдання 4: Підтвердити версію в ролауті (бо «тільки в прод» часто означає «тільки цей канарейка»)
cr0x@server:~$ kubectl -n web describe deploy/frontend | sed -n '1,120p'
Name: frontend
Namespace: web
Labels: app=frontend
Annotations: deployment.kubernetes.io/revision: 42
Containers:
frontend:
Image: registry.internal/frontend:sha-8c1d2a7
Ports: 8080/TCP
Що означає вивід: Ви перевіряєте запущене image і ревізію. Баги підказок часто корелюють з певним релізом.
Рішення: Якщо баг почався з ревізії, бісектіть через відкат або порівняння змін навколо коду підказок і CSS-шарів.
Завдання 5: Перевірити, чи reverse proxy не видаляє потрібні заголовки
cr0x@server:~$ curl -sS -I https://app.example.internal/ | grep -i -E "content-security-policy|x-frame-options|referrer-policy"
content-security-policy: default-src 'self'; script-src 'self'
referrer-policy: strict-origin-when-cross-origin
Що означає вивід: Заголовки безпеки можуть впливати на стратегії підказок (наприклад, якщо ви покладалися на inline-стилі). Політика фреймів важлива, якщо додаток вбудований.
Рішення: Якщо заголовки відрізняються між середовищами, вирівняйте їх; інакше ви продовжите деплоїти «працює в staging» підказки.
Завдання 6: Знайти, яким клієнтським маршрутом/версією користувачі користуються через access логи
cr0x@server:~$ sudo tail -n 200 /var/log/nginx/access.log | grep -E "GET /assets/|GET /settings" | tail -n 20
10.1.2.3 - - "GET /settings HTTP/2.0" 200 42103 "-" "Mozilla/5.0 ..."
10.1.2.3 - - "GET /assets/tooltip.js HTTP/2.0" 200 18342 "-" "Mozilla/5.0 ..."
Що означає вивід: Ви бачите, чи завантажується ассет підказки і чи повертається 200 або 304. Відсутній запит часто означає, що HTML не посилається на нього або клієнт блокує.
Рішення: Якщо не бачите запитів ассетів, перевірте збірку і включення в HTML. Якщо бачите 403/404 — виправляйте маршрути/ CDN.
Завдання 7: Перевірити, чи gzip/brotli не пошкоджує JS-пейлоад (рідко, але буває)
cr0x@server:~$ curl -sS -H 'Accept-Encoding: gzip' --compressed https://app.example.internal/assets/tooltip.js | head -n 5
(function(){'use strict';
var Tooltip=function(){...}
Що означає вивід: Ви підтверджуєте, що стиснута відповідь розпаковується в валідний JS-текст.
Рішення: Якщо отримуєте бінарний смітник або обрізаний контент, перевірте конфіг стиснення проксі.
Завдання 8: Виміряти клієнтські помилки в реальному часі через серверні логи (CSP reports)
cr0x@server:~$ kubectl -n web logs deploy/csp-report-collector --since=30m | tail -n 20
{"blocked-uri":"inline","violated-directive":"style-src-elem","document-uri":"https://app.example.internal/settings"}
Що означає вивід: Користувачі тригерять порушення CSP, які можуть перешкоджати стилям або видимості підказок.
Рішення: Приберіть inline-стилі/скрипти з реалізації або оновіть CSP з nonce/hash (ретельно).
Завдання 9: Підтвердити, що overlay root існує в шаблоні DOM (режим SSR/шаблонів)
cr0x@server:~$ curl -sS https://app.example.internal/ | grep -n 'id="overlays"' | head -n 5
118:
Що означає вивід: Якщо ваш портал очікує overlay root і він відсутній, підказки мовчазно проваляться або прикріпляться до body неправильно.
Рішення: Якщо відсутній — виправте базовий шаблон і додайте runtime-fallback, що створює вузол.
Завдання 10: Виявити, чи CSS-зміна додала overflow: hidden на ключовому предку
cr0x@server:~$ git show HEAD~1:ui/styles/components/card.css | sed -n '1,120p'
.card {
position: relative;
overflow: hidden;
border-radius: 12px;
}
Що означає вивід: Нещодавня зміна додала обрізання overflow. Це один із трьох головних вбивць підказок.
Рішення: Або порталити підказку з картки, або видалити overflow hidden, якщо воно не потрібно (зазвичай воно потрібно для заокруглених кутів).
Завдання 11: Відтворити обрізання локально в детермінованому середовищі
cr0x@server:~$ docker run --rm -p 8080:8080 registry.internal/frontend:sha-8c1d2a7
Listening on http://0.0.0.0:8080
Що означає вивід: Ви запускаєте той самий образ, що й у проді. Ніяких «працює на моєму ноуті» відговорок.
Рішення: Якщо відтворюється — дебажте в браузері спокійно. Якщо ні — різниця в конфігах, заголовках або upstream-слоях.
Завдання 12: Перевірити, чи відрізняються user-agents (торкання vs hover)
cr0x@server:~$ sudo awk -F\" '{print $6}' /var/log/nginx/access.log | tail -n 200 | sort | uniq -c | sort -nr | head
83 Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 ...
52 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...
Що означає вивід: Сплеск iPhone-трафіку може виявити помилки підказок, спричинені припущеннями про hover.
Рішення: Якщо мобільні пристрої домінують у скаргах, переконайтеся, що у вас є дружня до тапів поведінка і ви не покладаєтесь лише на hover.
Завдання 13: Визначити, чи код підказок викликає CPU-спайки (кореляція на стороні сервера)
cr0x@server:~$ kubectl -n web top pods | head
NAME CPU(cores) MEMORY(bytes)
frontend-6c7b9c9f8d-2kq9m 120m 310Mi
frontend-6c7b9c9f8d-l8z2p 115m 305Mi
Що означає вивід: Це не вимірює клієнтський CPU, але може показати корельовані стрибки (наприклад, SSR-рендеринг або надмірна генерація HTML).
Рішення: Якщо серверний CPU підстрибнув після релізу підказок, досліджуйте SSR-цикли, логування або зміни шаблонів, пов’язані з рендерингом підказок.
Завдання 14: Підтвердити, що source maps не відсутні в проді (для дебагованості)
cr0x@server:~$ ls -lh /srv/app/assets | grep -E 'tooltip\.js(\.map)?'
-rw-r--r-- 1 www-data www-data 18K Dec 29 09:58 tooltip.js
-rw-r--r-- 1 www-data www-data 61K Dec 29 09:58 tooltip.js.map
Що означає вивід: У вас є sourcemaps (навіть якщо під обмеженням). Дебажити геометрію підказки без sourcemaps — це самопошкодження.
Рішення: Якщо мапи відсутні, вирішіть, чи їх безпечно доставляти (обмежено), або покращіть серверне логування довкола рішень позиціонування підказок.
Три корпоративні міні-історії з шахт підказок
Міні-історія 1: Інцидент, спричинений хибним припущенням
Команда зробила чистий компонент підказки для білінгового дашборда. Він працював у деві, у стейджингу і на демо, де всі чемно користувалися мишею, ніби 2008 рік. Припущення: «підказки — це hover UI». Вони викотили це.
За день почалися звернення в сапорт: користувачі не могли зрозуміти, чому їхні рахунки падають. Іконка з помилкою мала єдине пояснення, і воно жило виключно в підказці. Багато користувачів навігаціювали клавіатурою через РСІ, а деякі використовували високий зум, де точність hover ускладнюється.
Першим «фіксом» стало відкривання підказки по кліку. Це зробило її більш доступною, але також блокувало іконку і сусідні посилання, створивши новий клас багів: кліки, що мали спрацювати на іконку, просто переключали підказку, в той час як скрінрідери отримували непослідовні оголошення, бо DOM підказки створювався лише після взаємодії.
Остаточний ремонт був нудним і ефективним: правильні вбудовані лейби для помилок, підказки зведені до коротких пояснень, і aria-describedby підключено до завжди присутнього елемента підказки, що відкривається на фокус. Інцидент не був про геометрію. Він був про семантику. Постмортем міг би називатись: «Ми використали підказку як мітку».
Міні-історія 2: Оптимізація, що обернулась проти
Інша організація вирішила, що підказки «дорогі», бо відкриття викликає вимірювання розкладки. Хтось профайлив повільну машину і вирішив: «Треба заздалегідь порахувати позиції для всіх підказок при завантаженні сторінки». Звучало ефективно. Була також неправильним у веб-формі.
Вони зробили prepass: пройтись по кожному тригеру підказки, виміряти rect-и і зберегти координати. Потім при hover рендерити підказку на збережених x/y. Сторінка мала сотні рядків, кожен з кількома іконками. Завантаження стало важчим, але в лабораторії ще прийнятним.
У продакшені користувачі фільтрували таблиці, змінювали ширину колонок і відкривали бічні панелі. Збережені координати застаріли. Підказки дрейфували, помилково переверталися і інколи з’являлися поза екраном. Гірше: сам prepass викликав примусові рефлоуси, роблячи сторінку повільною ще до взаємодії користувача.
Виправлення було протилежним: обчислювати позицію лише коли підказка відкрита і повторно обчислювати тільки при релевантних змінах (скрол/resize/observer), тротлінгом до кадрів. «Оптимізацію» замінили на «робити менше, пізніше, правильно».
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Компанія, що дбає про безпеку, ввела суворішу Content Security Policy. Це було добре. Воно також зламало вражаючу кількість UI-елементів, які тихо покладалися на inline-стилі.
Реалізація підказок була однією з них. Вони використовували швидкий трюк: задати style="top: ...px; left: ...px" під час виконання. CSP мала style-src 'self' без дозволів для inline-стилів, тому підказки рендерилися у дефолтній позиції CSS — прямо в лівому верхньому куті оверлейного шару.
Більшість команд просили винятків для CSP. Одна команда не просила. У них був пункт у чеклисті перед деплоєм: «Запустити CSP report-only у staging і переглянути порушення». Вони зробили маленький endpoint-колектор і справді подивилися на нього, бо втомилися від несподіваних п’ятниць.
Вони перенесли позиціонування підказок на CSS-змінні, оновлювані через stylesheet-правила замість inline-атрибутів, і забезпечили, що потрібний CSS в версіонованому ассеті. Коли суворіша політика увійшла в дію, їхні підказки продовжили працювати. Герой — чеклист і черга звітів. Гламурно? Ні. Ефективно? Надзвичайно.
Чеклисти / покроковий план
Покроково: побудуйте підказку, що переживе реальність
- Визначте семантику: підказка чи поповер. Якщо інтерактивна — це поповер, не імітуйте.
- Оберіть архітектуру: портал в топ-оверлей за замовчуванням.
- Створіть стійке підключення: згенеруйте ID підказки і встановіть
aria-describedbyна тригер. - Тримаєте DOM підказки присутнім: ховайте через
visibilityіopacity; не створюйте/знищуйте на кожному hover, якщо вам потрібна консистентність для скрінрідера. - Позиціонуйте з fixed + transform: обчислюйте viewport x/y; застосовуйте через
translate3d. - Реалізуйте flip + shift: не дозволяйте підказкам виходити за екран. Зафіксуйте з padding.
- Стрілка слідує фінальному розміщенню: обчисліть offset стрілки після shift; зафіксуйте, щоб уникнути кутів.
- Події: відкривати на hover і focus; закривати на leave/blur/Escape; закривати на pointerdown поза для touch.
- Тротлінг оновлень: rAF для скролу/resize; спостерігачі лише коли відкрито.
- Тестування: додайте детерміністичні селектори і юніт-тестуйте геометричну функцію з фікстурами прямокутників.
- Ущільнення: обробіть відсутній overlay root; захищайтеся від null-ref; розглядайте CSP як вхідний дизайн-параметр.
Чеклист: етапи готовності для продакшену
- Працює з навігацією лише клавіатурою (фокус відкриває, Escape закриває).
- Оголошується скрінрідерами (role + described-by + стабільний DOM).
- Переживає вкладені скрол-контейнери (репозиціонування на скрол).
- Не обрізається overflow (портал).
- Немає циклів мерехтіння (offset + pointer-events стратегія + затримка).
- Не викликає помітного джанку при скролі (тротлінг вимірювань, без репозиціонування на mousemove).
- Має визначену стратегію z-index шарів (overlay root вище хедерів/модалів).
- Сумісний з CSP (без inline-style, якщо політика забороняє).
Поширені питання
- 1) Чи можна просто використати HTML-атрибут
title? - Можна, але ви втратите контроль стилів, отримаєте непослідовні таймінги між браузерами і часто слабку доступність. Використовуйте тільки як крайню резервну опцію.
- 2) Чи має вміст підказки бути в DOM, коли він прихований?
- Зазвичай так. Це робить
aria-describedbyстабільним і покращує консистентність для скрінрідерів. Ховайте черезvisibility: hiddenіopacity: 0, а неdisplay: none, якщо потрібно, щоб елемент залишався референсом. - 3) Яку ARIA-роль слід використовувати?
role="tooltip"на елементі підказки. Потім приєднати до тригера черезaria-describedby. Тримайте вміст неінтерактивним.- 4) Чи повинні підказки відкриватися по кліку?
- На сенсорних пристроях тап/клік може бути розумним тригером. На десктопі віддавайте перевагу hover + focus. Якщо відкривати по кліку — потрібно визначити правила закриття і забезпечити, щоб підказка не блокувала наступну дію.
- 5) Чому мій z-index підказки не працює?
- Бо z-index конкурує лише в межах одного контексту стекування. Трансформи, opacity та певні CSS-властивості створюють нові контексти. Порталите в відомий overlay root і централізовано керуйте z-index.
- 6) Як запобігти обрізанню в overflow-контейнерах?
- Порталте підказку поза контейнер (зазвичай до
document.bodyабо overlay root) і позиціонуйте за координатами вьюпорта зposition: fixed. - 7) Як часто перепозиціонувати підказку?
- При відкритті, а потім коли щось релевантне змінюється: скрол, resize, зміна розміру тригера або зміна вмісту підказки. Тротліть до одного оновлення на кадр.
- 8) Чи можна анімувати підказки безпечно?
- Так: анімуйте opacity і transform (невеликий translate). Уникайте анімації top/left. Тримайте анімації короткими і поважайте налаштування reduced-motion, якщо це потрібно продукту.
- 9) Чи має підказка бути hoverable?
- Якщо вона неінтерактивна, їй не потрібно бути hoverable. Використовуйте
pointer-events: noneі уникайте мерехтіння. Якщо потрібна витримка для читання довшого тексту — додайте затримку закриття і дозволіть вказівнику заходити в підказку. - 10) Яка найпростіша надійна стратегія розміщення?
- Переважний бік + flip, якщо не вміщається + shift, щоб триматися у вьюпорті + clamp з padding. Не пропускайте shift/clamp, якщо ваш UI може скролитись або міняти розмір — це казка, що не існує.
Висновок: наступні кроки, які вас не підведуть
Якщо ви хочете підказки без бібліотек, трюк не в написанні хитрого коду. Він у тому, щоб писати код, який припускає, що ваша розкладка буде ворожою, користувачі — різноманітними, а ваш CSS — «покращений» кимось, хто не знає про ваш компонент.
Наступні кроки, що швидко окупляться:
- Обрати портал + fixed позиціонування як архітектуру за замовчуванням.
- Реалізувати геометрію як чисту функцію і юніт-тестувати її з прямокутниками.
- Підключити
aria-describedby+role="tooltip"і відкривати на фокус, а не тільки на hover. - Додати rAF-тротлінг для скрол/resize репозиціонування і перестати вимірювати на mousemove.
- Прогнати плейбук діагностики на сторінці з модалами, липкими хедерами і вкладеними скрол-панелями — бо саме там живе правда.
Операційне мислення: Ставтесь до підказок як до будь-якої іншої продакшен-фічі. Вони мають залежності (CSP, шари z-index, скрол-контейнери) і режими відмови. Зробіть їх спостережуваними і нудними.