Задача
СкопированоТултип — небольшая всплывающая подсказка с дополнительной информацией о функциях интерактивного элемента. Появляется при наведении курсора на элемент или взаимодействии с ним с клавиатуры. Это распространённый паттерн в веб-интерфейсах.
Тултип полезен, когда у элемента нет подписи, только иконка. Или когда нужно подробнее раскрыть его назначение.
Тултип может быть двух типов:
- статичный — будет показываться только с одной, заранее выбранной, стороны;
- адаптивный — будет появляться сверху, справа, снизу и слева от элемента в зависимости от наличия свободного места на экране.
В статье разберём три способа создания тултипа:
- Статичный.
- Адаптивный на основе Intersection Observer.
- Адаптивный на основе Popover API и CSS Anchor Positioning.
Статичный
СкопированоГотовое решение
СкопированоДля начала создадим HTML-разметку со всеми необходимыми элементами:
<div class="tooltip-container"> <button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip" > ❤️ </button> <div class="tooltip hidden" role="tooltip" id="tooltip" data-position="top" > Добавить в «Избранное» </div></div>
<div class="tooltip-container" > <button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip" > ❤️ </button> <div class="tooltip hidden" role="tooltip" id="tooltip" data-position="top" > Добавить в «Избранное» </div> </div>
Внешнее оформление тултипа и элемента, к которому он привязан, опишем с помощью следующих CSS-правил:
.button { display: block; min-width: 210px; border: 2px solid transparent; border-radius: 6px; padding: 9px 15px; color: #000000; font-size: inherit; font-weight: 300; font-family: inherit; transition: background-color 0.15s ease-in; cursor: pointer; background-color: #C56FFF;}.button:hover { background-color: #FFFFFF;}.button:focus-visible { border: 2px solid #FFFFFF; outline: none;}.button:focus { border: 2px solid #FFFFFF; outline: none;}@media (width < 768px) { .button { min-width: 60px; }}.tooltip-container { position: relative; display: inline-block;}.tooltip { position: absolute; width: max-content; max-width: 300px; padding: 10px 40px; background-color: #FFFFFF; color: #000000; text-align: center;}.tooltip::after { content: ''; position: absolute; border: 7px solid transparent;}.tooltip[data-position=top] { bottom: calc(100% + 14px); left: 50%; translate: -50% 0;}.tooltip[data-position=top]::after { bottom: -14px; left: 50%; translate: -50% 0; border-top-color: #FFFFFF;}.tooltip[data-position=bottom] { top: calc(100% + 14px); left: 50%; translate: -50% 0;}.tooltip[data-position=bottom]::after { top: -14px; left: 50%; translate: -50% 0; border-bottom-color: #FFFFFF;}.tooltip[data-position=right] { left: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px;}.tooltip[data-position=right]::after { left: -14px; top: 50%; translate: 0 -50%; border-right-color: #FFFFFF;}.tooltip[data-position=left] { right: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px;}.tooltip[data-position=left]::after { right: -14px; top: 50%; translate: 0 -50%; border-left-color: #FFFFFF;}.hidden { visibility: hidden;}
.button { display: block; min-width: 210px; border: 2px solid transparent; border-radius: 6px; padding: 9px 15px; color: #000000; font-size: inherit; font-weight: 300; font-family: inherit; transition: background-color 0.15s ease-in; cursor: pointer; background-color: #C56FFF; } .button:hover { background-color: #FFFFFF; } .button:focus-visible { border: 2px solid #FFFFFF; outline: none; } .button:focus { border: 2px solid #FFFFFF; outline: none; } @media (width < 768px) { .button { min-width: 60px; } } .tooltip-container { position: relative; display: inline-block; } .tooltip { position: absolute; width: max-content; max-width: 300px; padding: 10px 40px; background-color: #FFFFFF; color: #000000; text-align: center; } .tooltip::after { content: ''; position: absolute; border: 7px solid transparent; } .tooltip[data-position=top] { bottom: calc(100% + 14px); left: 50%; translate: -50% 0; } .tooltip[data-position=top]::after { bottom: -14px; left: 50%; translate: -50% 0; border-top-color: #FFFFFF; } .tooltip[data-position=bottom] { top: calc(100% + 14px); left: 50%; translate: -50% 0; } .tooltip[data-position=bottom]::after { top: -14px; left: 50%; translate: -50% 0; border-bottom-color: #FFFFFF; } .tooltip[data-position=right] { left: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px; } .tooltip[data-position=right]::after { left: -14px; top: 50%; translate: 0 -50%; border-right-color: #FFFFFF; } .tooltip[data-position=left] { right: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px; } .tooltip[data-position=left]::after { right: -14px; top: 50%; translate: 0 -50%; border-left-color: #FFFFFF; } .hidden { visibility: hidden; }
Реализуем отображение и скрытие тултипа с помощью JavaScript-методов:
const tooltip = document.querySelector('#tooltip')const tooltipAnchor = document.querySelector('#tooltip-anchor')const showTooltip = () => { tooltip.classList.remove('hidden')}const hideTooltip = () => { tooltip.classList.add('hidden')}tooltipAnchor.addEventListener('mouseenter', showTooltip)tooltipAnchor.addEventListener('focus', showTooltip)tooltipAnchor.addEventListener('mouseleave', hideTooltip)tooltipAnchor.addEventListener('blur', hideTooltip)tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() }})
const tooltip = document.querySelector('#tooltip') const tooltipAnchor = document.querySelector('#tooltip-anchor') const showTooltip = () => { tooltip.classList.remove('hidden') } const hideTooltip = () => { tooltip.classList.add('hidden') } tooltipAnchor.addEventListener('mouseenter', showTooltip) tooltipAnchor.addEventListener('focus', showTooltip) tooltipAnchor.addEventListener('mouseleave', hideTooltip) tooltipAnchor.addEventListener('blur', hideTooltip) tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() } })
Статичный тултип может показываться с одной из четырех сторон, но сторона должна быть выбрана заранее. «На лету» статичный тултип не сможет подстроиться под изменения на экране. Он будет продолжать показываться с выбранной стороны, даже если она скрыта за вьюпортом.
Разбор решения
СкопированоВ качестве интерактивного элемента будем использовать кнопку, а с помощью атрибута aria
свяжем её с тултипом.
За основу тултипа возьмём <div>
. Добавим ему соответствующую role
— tooltip
, чтобы ассистивные технологии понимали назначение элемента.
Обернём кнопку и тултип в контейнер — относительно него мы спозиционируем всплывающую подсказку.
Разметка
Скопировано<div class="tooltip-container"> <button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip" > ❤️ </button> <div class="tooltip hidden" role="tooltip" id="tooltip" data-position="top" > Добавить в «Избранное» </div></div>
<div class="tooltip-container" > <button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip" > ❤️ </button> <div class="tooltip hidden" role="tooltip" id="tooltip" data-position="top" > Добавить в «Избранное» </div> </div>
Стили
СкопированоСделаем контейнер строчно-блочным и зададим для свойства position
значение relative
. Это позволит позиционировать тултип относительно кнопки.
.tooltip-container { position: relative; display: inline-block;}
.tooltip-container { position: relative; display: inline-block; }
Опишем базовые стили для тултипа и его хвостика.
.tooltip { position: absolute; width: max-content; max-width: 300px; padding: 10px 40px; background-color: #FFFFFF; color: #000000; text-align: center;}/* хвостик тултипа */.tooltip::after { content: ''; position: absolute; border: 7px solid transparent;}
.tooltip { position: absolute; width: max-content; max-width: 300px; padding: 10px 40px; background-color: #FFFFFF; color: #000000; text-align: center; } /* хвостик тултипа */ .tooltip::after { content: ''; position: absolute; border: 7px solid transparent; }
Зададим стили для каждого из четырёх положений.
/* Тутлтип сверху */.tooltip[data-position=top] { bottom: calc(100% + 14px); left: 50%; translate: -50% 0;}/* Хвостик тутлтипа сверху */.tooltip[data-position=top]::after { bottom: -14px; left: 50%; translate: -50% 0; border-top-color: #FFFFFF;}/* Тутлтип снизу */.tooltip[data-position=bottom] { top: calc(100% + 14px); left: 50%; translate: -50% 0;}/* Хвостик тутлтипа снизу */.tooltip[data-position=bottom]::after { top: -14px; left: 50%; translate: -50% 0; border-bottom-color: #FFFFFF;}/* Тутлтип справа */.tooltip[data-position=right] { left: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px;}/* Хвостик тутлтипа справа */.tooltip[data-position=right]::after { left: -14px; top: 50%; translate: 0 -50%; border-right-color: #FFFFFF;}/* Тутлтип снизу */.tooltip[data-position=left] { right: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px;}/* Хвостик тутлтипа снизу */.tooltip[data-position=left]::after { right: -14px; top: 50%; translate: 0 -50%; border-left-color: #FFFFFF;}
/* Тутлтип сверху */ .tooltip[data-position=top] { bottom: calc(100% + 14px); left: 50%; translate: -50% 0; } /* Хвостик тутлтипа сверху */ .tooltip[data-position=top]::after { bottom: -14px; left: 50%; translate: -50% 0; border-top-color: #FFFFFF; } /* Тутлтип снизу */ .tooltip[data-position=bottom] { top: calc(100% + 14px); left: 50%; translate: -50% 0; } /* Хвостик тутлтипа снизу */ .tooltip[data-position=bottom]::after { top: -14px; left: 50%; translate: -50% 0; border-bottom-color: #FFFFFF; } /* Тутлтип справа */ .tooltip[data-position=right] { left: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px; } /* Хвостик тутлтипа справа */ .tooltip[data-position=right]::after { left: -14px; top: 50%; translate: 0 -50%; border-right-color: #FFFFFF; } /* Тутлтип снизу */ .tooltip[data-position=left] { right: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px; } /* Хвостик тутлтипа снизу */ .tooltip[data-position=left]::after { right: -14px; top: 50%; translate: 0 -50%; border-left-color: #FFFFFF; }
Визуально скрывать тултип будем с помощью свойства visibility
.
.hidden { visibility: hidden;}
.hidden { visibility: hidden; }
JavaScript
СкопированоДля начала найдём все элементы, которые понадобятся нам для работы с тултипом: сам тултип и кнопку, к которой он привязан.
const tooltip = document.querySelector('#tooltip')const tooltipAnchor = document.querySelector('#tooltip-anchor')
const tooltip = document.querySelector('#tooltip') const tooltipAnchor = document.querySelector('#tooltip-anchor')
Напишем функции для отображения и скрытия тултипа.
const showTooltip = () => { tooltip.classList.remove('hidden')}const hideTooltip = () => { tooltip.classList.add('hidden')}
const showTooltip = () => { tooltip.classList.remove('hidden') } const hideTooltip = () => { tooltip.classList.add('hidden') }
Навесим соответствующие обработчики событий на кнопку. Теперь она умеет показывать тултип на наведение курсора мыши или фокусе с клавиатуры. Закрываться тултип будет при потере кнопкой ховера или фокуса, а также при нажатии на клавишу Escape.
tooltipAnchor.addEventListener('mouseenter', showTooltip)tooltipAnchor.addEventListener('focus', showTooltip)tooltipAnchor.addEventListener('mouseleave', hideTooltip)tooltipAnchor.addEventListener('blur', hideTooltip)tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() }})
tooltipAnchor.addEventListener('mouseenter', showTooltip) tooltipAnchor.addEventListener('focus', showTooltip) tooltipAnchor.addEventListener('mouseleave', hideTooltip) tooltipAnchor.addEventListener('blur', hideTooltip) tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() } })
Адаптивный на основе Intersection Observer
СкопированоПользователь может по-разному взаимодействовать с интерфейсом. Он может вызвать всплывающую подсказку в начале страницы или пролистав её почти до конца. В этот момент статично заданое положение тултипа может быть скрыто за границами вьюпорта. Избежать подобной ситуации поможет тултип, который умеет адаптироваться под свободное место на экране.
Разберём рецент адаптивного тултипа.
Готовое решение
СкопированоОпишем необходимую HTML-разметку:
<div class="tooltip-container"> <button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip" > ❤️ </button> <div class="tooltip hidden" role="tooltip" id="tooltip" data-position="top" > Добавить в «Избранное» </div></div>
<div class="tooltip-container" > <button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip" > ❤️ </button> <div class="tooltip hidden" role="tooltip" id="tooltip" data-position="top" > Добавить в «Избранное» </div> </div>
Добавим стилизацию:
.button { display: block; min-width: 210px; border: 2px solid transparent; border-radius: 6px; padding: 9px 15px; color: #000000; font-size: inherit; font-weight: 300; font-family: inherit; transition: background-color 0.15s ease-in; cursor: pointer; background-color: #C56FFF;}.button:hover { background-color: #FFFFFF;}.button:focus-visible { border: 2px solid #FFFFFF; outline: none;}.button:focus { border: 2px solid #FFFFFF; outline: none;}@media (width < 768px) { .button { min-width: 60px; }}.tooltip-container { position: relative; display: inline-block;}.tooltip { position: absolute; width: max-content; max-width: 300px; padding: 10px 40px; background-color: #FFFFFF; color: #000000; text-align: center;}.tooltip::after { content: ''; position: absolute; border: 7px solid transparent;}.tooltip[data-position=top] { bottom: calc(100% + 14px); left: 50%; translate: -50% 0;}.tooltip[data-position=top]::after { bottom: -14px; left: 50%; translate: -50% 0; border-top-color: #FFFFFF;}.tooltip[data-position=bottom] { top: calc(100% + 14px); left: 50%; translate: -50% 0;}.tooltip[data-position=bottom]::after { top: -14px; left: 50%; translate: -50% 0; border-bottom-color: #FFFFFF;}.tooltip[data-position=right] { left: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px;}.tooltip[data-position=right]::after { left: -14px; top: 50%; translate: 0 -50%; border-right-color: #FFFFFF;}.tooltip[data-position=left] { right: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px;}.tooltip[data-position=left]::after { right: -14px; top: 50%; translate: 0 -50%; border-left-color: #FFFFFF;}.hidden { visibility: hidden;}
.button { display: block; min-width: 210px; border: 2px solid transparent; border-radius: 6px; padding: 9px 15px; color: #000000; font-size: inherit; font-weight: 300; font-family: inherit; transition: background-color 0.15s ease-in; cursor: pointer; background-color: #C56FFF; } .button:hover { background-color: #FFFFFF; } .button:focus-visible { border: 2px solid #FFFFFF; outline: none; } .button:focus { border: 2px solid #FFFFFF; outline: none; } @media (width < 768px) { .button { min-width: 60px; } } .tooltip-container { position: relative; display: inline-block; } .tooltip { position: absolute; width: max-content; max-width: 300px; padding: 10px 40px; background-color: #FFFFFF; color: #000000; text-align: center; } .tooltip::after { content: ''; position: absolute; border: 7px solid transparent; } .tooltip[data-position=top] { bottom: calc(100% + 14px); left: 50%; translate: -50% 0; } .tooltip[data-position=top]::after { bottom: -14px; left: 50%; translate: -50% 0; border-top-color: #FFFFFF; } .tooltip[data-position=bottom] { top: calc(100% + 14px); left: 50%; translate: -50% 0; } .tooltip[data-position=bottom]::after { top: -14px; left: 50%; translate: -50% 0; border-bottom-color: #FFFFFF; } .tooltip[data-position=right] { left: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px; } .tooltip[data-position=right]::after { left: -14px; top: 50%; translate: 0 -50%; border-right-color: #FFFFFF; } .tooltip[data-position=left] { right: calc(100% + 14px); top: 50%; translate: 0 -50%; max-width: 200px; } .tooltip[data-position=left]::after { right: -14px; top: 50%; translate: 0 -50%; border-left-color: #FFFFFF; } .hidden { visibility: hidden; }
Реализуем методы отображения, скрытия и изменения положения тултипа:
const tooltip = document.querySelector('#tooltip')const tooltipAnchor = document.querySelector('#tooltip-anchor')const showTooltip = () => { tooltip.classList.remove('hidden')}const hideTooltip = () => { tooltip.classList.add('hidden')}tooltipAnchor.addEventListener('mouseenter', showTooltip)tooltipAnchor.addEventListener('focus', showTooltip)tooltipAnchor.addEventListener('mouseleave', hideTooltip)tooltipAnchor.addEventListener('blur', hideTooltip)tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() }})const defineTooltipPosition = (boundingClientRect, intersectionRect) => { if (boundingClientRect.bottom > intersectionRect.bottom) { return 'top' } else if (boundingClientRect.right > intersectionRect.right) { return 'left' } else if (boundingClientRect.left < intersectionRect.left) { return 'right' } else if (boundingClientRect.top < intersectionRect.top) { return 'bottom' } else { return 'top' }}const observerOptions = { root: document, rootMargin: '-20px', threshold: 1}const observerCallback = (entries) => { entries.forEach(({ isIntersecting, boundingClientRect, intersectionRect }) => { if (!isIntersecting) { tooltip.dataset.position = defineTooltipPosition(boundingClientRect, intersectionRect) } })}const observer = new IntersectionObserver(observerCallback, observerOptions)observer.observe(tooltip)
const tooltip = document.querySelector('#tooltip') const tooltipAnchor = document.querySelector('#tooltip-anchor') const showTooltip = () => { tooltip.classList.remove('hidden') } const hideTooltip = () => { tooltip.classList.add('hidden') } tooltipAnchor.addEventListener('mouseenter', showTooltip) tooltipAnchor.addEventListener('focus', showTooltip) tooltipAnchor.addEventListener('mouseleave', hideTooltip) tooltipAnchor.addEventListener('blur', hideTooltip) tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() } }) const defineTooltipPosition = (boundingClientRect, intersectionRect) => { if (boundingClientRect.bottom > intersectionRect.bottom) { return 'top' } else if (boundingClientRect.right > intersectionRect.right) { return 'left' } else if (boundingClientRect.left < intersectionRect.left) { return 'right' } else if (boundingClientRect.top < intersectionRect.top) { return 'bottom' } else { return 'top' } } const observerOptions = { root: document, rootMargin: '-20px', threshold: 1 } const observerCallback = (entries) => { entries.forEach(({ isIntersecting, boundingClientRect, intersectionRect }) => { if (!isIntersecting) { tooltip.dataset.position = defineTooltipPosition(boundingClientRect, intersectionRect) } }) } const observer = new IntersectionObserver(observerCallback, observerOptions) observer.observe(tooltip)
Попробуйте проскроллить к разным краям вьюпорта — тултип сможет адаптироваться.
Разбор решения
СкопированоРазметка и стили
СкопированоРазметка и стили точно такие же, как в рецепте статичного тултипа. Повторно их разбирать не будем.
JavaScript
СкопированоМетоды отображения и скрытия тултипа идентичны методам статичного тултипа.
Разберём логику переноса тултипа в свободную область экрана.
Сперва опишем метод определения положения тултипа. Если какая-то из сторон всплывающей подсказки будет близка к выходу за границы окна браузера, начнем показывать его с противоположной стороны кнопки.
const defineTooltipPosition = (boundingClientRect, intersectionRect) => { // Если тултип выходит за нижнюю границу, показываем его сверху if (boundingClientRect.bottom > intersectionRect.bottom) { return 'top' // Выходит за правую — показываем слева } else if (boundingClientRect.right > intersectionRect.right) { return 'left' // Выходит за левую — показываем справа } else if (boundingClientRect.left < intersectionRect.left) { return 'right' // Выходит за верхнюю — показываем снизу } else if (boundingClientRect.top < intersectionRect.top) { return 'bottom' // Дефолтное положение — сверху } else { return 'top' }}
const defineTooltipPosition = (boundingClientRect, intersectionRect) => { // Если тултип выходит за нижнюю границу, показываем его сверху if (boundingClientRect.bottom > intersectionRect.bottom) { return 'top' // Выходит за правую — показываем слева } else if (boundingClientRect.right > intersectionRect.right) { return 'left' // Выходит за левую — показываем справа } else if (boundingClientRect.left < intersectionRect.left) { return 'right' // Выходит за верхнюю — показываем снизу } else if (boundingClientRect.top < intersectionRect.top) { return 'bottom' // Дефолтное положение — сверху } else { return 'top' } }
Далее применим этот метод внутри колбэка для Intersection
.
/* Отслеживаем приближение тултипа к границе вьюпорта меньше, чем на 20 пикселей*/const observerOptions = { root: document, rootMargin: '-20px', threshold: 1}const observerCallback = (entries) => { entries.forEach(({ isIntersecting, boundingClientRect, intersectionRect }) => { if (!isIntersecting) { // Если пересёк установленную границу — обновляем положение тултипа tooltip.dataset.position = defineTooltipPosition(boundingClientRect, intersectionRect) } })}const observer = new IntersectionObserver(observerCallback, observerOptions)observer.observe(tooltip)
/* Отслеживаем приближение тултипа к границе вьюпорта меньше, чем на 20 пикселей */ const observerOptions = { root: document, rootMargin: '-20px', threshold: 1 } const observerCallback = (entries) => { entries.forEach(({ isIntersecting, boundingClientRect, intersectionRect }) => { if (!isIntersecting) { // Если пересёк установленную границу — обновляем положение тултипа tooltip.dataset.position = defineTooltipPosition(boundingClientRect, intersectionRect) } }) } const observer = new IntersectionObserver(observerCallback, observerOptions) observer.observe(tooltip)
Адаптивный на основе Popover API и CSS Anchor Positioning
СкопированоВ предыдущих рецептах мы управляли видимостью тултипа с помощью смены CSS-классов и меняли расположние тултипа с помощью Intersection Observer.
Теперь подобную функциональность можно реализовать проще и с меньшим количеством JavaScript благодаря Popover API и CSS Anchor Positioning.
Готовое решение
СкопированоДля начала создадим HTML-разметку со всеми необходимыми элементами:
<button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip"> ❤️</button><div class="tooltip" id="tooltip" role="tooltip" popover="manual"> <div class="tooltip-content"> Добавить в «Избранное» </div></div>
<button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip" > ❤️ </button> <div class="tooltip" id="tooltip" role="tooltip" popover="manual" > <div class="tooltip-content"> Добавить в «Избранное» </div> </div>
Внешнее оформление тултипа и его умение адаптироваться под местоположение элемента, к которому он привязан, опишем с помощью следующих CSS-правил:
.button { display: block; min-width: 210px; border: 2px solid transparent; border-radius: 6px; padding: 9px 15px; color: #000000; font-size: inherit; font-weight: 300; font-family: inherit; transition: background-color 0.15s ease-in; cursor: pointer; background-color: #C56FFF;}.button:hover { background-color: #FFFFFF;}.button:focus-visible { border: 2px solid #FFFFFF; outline: none;}.button:focus { border: 2px solid #FFFFFF; outline: none;}@media (width < 768px) { .button { min-width: 60px; }}.tooltip-anchor { anchor-name: --button-el;}.tooltip { inset: unset; max-width: 300px; margin: 10px; padding: 10px 40px; background-color: #FFFFFF; color: #000000; text-align: center; position-anchor: --button-el; position-area: top; position-try-fallbacks: --bottom, --left, --right; anchor-name: --tooltip-el;}@position-try --bottom { position-area: bottom;}@position-try --left { position-area: left; max-width: 200px;}@position-try --right { position-area: right; max-width: 200px;}.tooltip-content { background-color: inherit;}.tooltip::before,.tooltip::after,.tooltip .tooltip-content::before,.tooltip .tooltip-content::after { position-anchor: --button-el; content: ''; position: fixed; background-color: inherit; margin: auto;}.tooltip::before,.tooltip::after { left: anchor(--button-el start); right: anchor(--button-el end); width: 10px; max-height: 10px;}.tooltip::before { top: anchor(--button-el end); bottom: anchor(--tooltip-el start); translate: 0 7px; rotate: 45deg;}.tooltip::after { top: anchor(--tooltip-el end); bottom: anchor(--button-el start); translate: 0 -7px; rotate: 45deg;}.tooltip .tooltip-content::before,.tooltip .tooltip-content::after { top: anchor(--button-el start); bottom: anchor(--button-el end); height: 10px; max-width: 10px;}.tooltip .tooltip-content::before { left: anchor(--button-el end); right: anchor(--tooltip-el start); translate: 7px; rotate: 45deg;}.tooltip .tooltip-content::after { left: anchor(--tooltip-el end); right: anchor(--button-el start); translate: -7px; rotate: 45deg;}
.button { display: block; min-width: 210px; border: 2px solid transparent; border-radius: 6px; padding: 9px 15px; color: #000000; font-size: inherit; font-weight: 300; font-family: inherit; transition: background-color 0.15s ease-in; cursor: pointer; background-color: #C56FFF; } .button:hover { background-color: #FFFFFF; } .button:focus-visible { border: 2px solid #FFFFFF; outline: none; } .button:focus { border: 2px solid #FFFFFF; outline: none; } @media (width < 768px) { .button { min-width: 60px; } } .tooltip-anchor { anchor-name: --button-el; } .tooltip { inset: unset; max-width: 300px; margin: 10px; padding: 10px 40px; background-color: #FFFFFF; color: #000000; text-align: center; position-anchor: --button-el; position-area: top; position-try-fallbacks: --bottom, --left, --right; anchor-name: --tooltip-el; } @position-try --bottom { position-area: bottom; } @position-try --left { position-area: left; max-width: 200px; } @position-try --right { position-area: right; max-width: 200px; } .tooltip-content { background-color: inherit; } .tooltip::before, .tooltip::after, .tooltip .tooltip-content::before, .tooltip .tooltip-content::after { position-anchor: --button-el; content: ''; position: fixed; background-color: inherit; margin: auto; } .tooltip::before, .tooltip::after { left: anchor(--button-el start); right: anchor(--button-el end); width: 10px; max-height: 10px; } .tooltip::before { top: anchor(--button-el end); bottom: anchor(--tooltip-el start); translate: 0 7px; rotate: 45deg; } .tooltip::after { top: anchor(--tooltip-el end); bottom: anchor(--button-el start); translate: 0 -7px; rotate: 45deg; } .tooltip .tooltip-content::before, .tooltip .tooltip-content::after { top: anchor(--button-el start); bottom: anchor(--button-el end); height: 10px; max-width: 10px; } .tooltip .tooltip-content::before { left: anchor(--button-el end); right: anchor(--tooltip-el start); translate: 7px; rotate: 45deg; } .tooltip .tooltip-content::after { left: anchor(--tooltip-el end); right: anchor(--button-el start); translate: -7px; rotate: 45deg; }
Реализуем отображение и скрытие тултипа с помощью JavaScript-методов:
const tooltip = document.querySelector('#tooltip')const tooltipAnchor = document.querySelector('#tooltip-anchor')const showTooltip = () => { tooltip.showPopover()}const hideTooltip = () => { tooltip.hidePopover()}tooltipAnchor.addEventListener('mouseenter', showTooltip)tooltipAnchor.addEventListener('focus', showTooltip)tooltipAnchor.addEventListener('mouseleave', hideTooltip)tooltipAnchor.addEventListener('blur', hideTooltip)tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() }})
const tooltip = document.querySelector('#tooltip') const tooltipAnchor = document.querySelector('#tooltip-anchor') const showTooltip = () => { tooltip.showPopover() } const hideTooltip = () => { tooltip.hidePopover() } tooltipAnchor.addEventListener('mouseenter', showTooltip) tooltipAnchor.addEventListener('focus', showTooltip) tooltipAnchor.addEventListener('mouseleave', hideTooltip) tooltipAnchor.addEventListener('blur', hideTooltip) tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() } })
Попробуйте проскроллить к разным краям вьюпорта — тултип сможет адаптироваться.
Разбор решения
СкопированоРазметка
СкопированоРазметка похожа на разметку в предыдущих рецептах, но есть некоторые отличия.
В качестве интерактивного элемента по-прежнему будем использовать кнопку и свяжем её с тултипом с помощью атрибута aria
.
Основа тултипа, как и раньше, <div>
с ролью tooltip
. Из новинок — добавим атрибут popover
, чтобы переключать видимость тултипа. Значение manual
необходимо, чтобы тултип не скрывался при клике на кнопку.
В рецепте мы специально не связываем тултип с кнопкой с помощью атрибута popovertarget
, так как хотим показывать тултип на ховер или фокус, а не на клик.
Также в данном рецепте не требуется дополнительный контейнер вокруг кнопки и тултипа, «привяжем» тултип напрямую к кнопке.
<button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip"> ❤️</button><div class="tooltip" id="tooltip" role="tooltip" popover="manual"> <div class="tooltip-content"> Добавить в «Избранное» </div></div>
<button class="button tooltip-anchor" id="tooltip-anchor" aria-describedby="tooltip" > ❤️ </button> <div class="tooltip" id="tooltip" role="tooltip" popover="manual" > <div class="tooltip-content"> Добавить в «Избранное» </div> </div>
Стили
СкопированоСперва необходимо связать кнопку с тултипом. Для этого дадим кнопке «якорное имя» -
и затем сошлёмся на него в свойстве position
.
.tooltip-anchor { /* Даём якорю (кнопке) имя */ anchor-name: --button-el;}.tooltip { /* Ссылаемся на якорь, к которому привязан тултип */ position-anchor: --button-el;}
.tooltip-anchor { /* Даём якорю (кнопке) имя */ anchor-name: --button-el; } .tooltip { /* Ссылаемся на якорь, к которому привязан тултип */ position-anchor: --button-el; }
Далее опишем доступные положения тултипа относительно кнопки. Дефолтное — top
. Если расположить тултип сверху не удалось, попросим браузер отрисовать тултип снизу, слева или справа через фолбэки -
, -
и -
.
.tooltip { /* Выставляем дефолтное положение относительно якоря */ position-area: top; /* Добавляем фолбэки, если дефолт не отработал */ position-try-fallbacks: --bottom, --left, --right;}/* Описываем, что значат фолбэки */@position-try --bottom { position-area: bottom;}@position-try --left { position-area: left; max-width: 200px;}@position-try --right { position-area: right; max-width: 200px;}
.tooltip { /* Выставляем дефолтное положение относительно якоря */ position-area: top; /* Добавляем фолбэки, если дефолт не отработал */ position-try-fallbacks: --bottom, --left, --right; } /* Описываем, что значат фолбэки */ @position-try --bottom { position-area: bottom; } @position-try --left { position-area: left; max-width: 200px; } @position-try --right { position-area: right; max-width: 200px; }
Создадим хвостики тултипа с помощью псевдоэлементов :
и :
. Фактически, у нас будет 4 хвостика, но в каждом из возможных четырёх положений тултипа (сверху, снизу, справа, слева) увидим только один.
/* Хвостики */.tooltip::before,.tooltip::after,.tooltip .tooltip-content::before,.tooltip .tooltip-content::after { /* Якоримся на тот же элемент, что и тултип */ position-anchor: --button-el; content: ''; position: fixed; background: inherit; margin: auto;}
/* Хвостики */ .tooltip::before, .tooltip::after, .tooltip .tooltip-content::before, .tooltip .tooltip-content::after { /* Якоримся на тот же элемент, что и тултип */ position-anchor: --button-el; content: ''; position: fixed; background: inherit; margin: auto; }
Сделаем тултип якорем. Это нужно для управления видимостью хвостиков.
.tooltip-anchor { /* Делаем тултип якорем */ anchor-name: --tooltip-el;}
.tooltip-anchor { /* Делаем тултип якорем */ anchor-name: --tooltip-el; }
Опишем стили, которые будут управлять видимостью хвостиков. Мы будем растягивать хвостики по высоте между двумя якорями: -
и -
. Хвостик, у которого высота примет положительное значение, будет виден, остальные нет.
Хвостики для вертикальной ориентации опишем с помощью :
и :
у элемента .tooltip
.
/* Для вертикальной ориентации */.tooltip::before,.tooltip::after { left: anchor(--button-el start); right: anchor(--button-el end); width: 10px; max-height: 10px;}/* Для случая, когда тултип под кнопкой */.tooltip::before { /* Растягиваем хвостик между двумя якорями */ top: anchor(--button-el end); bottom: anchor(--tooltip-el start); translate: 0 7px; rotate: 45deg;}/* Для случая, когда тултип над кнопкой */.tooltip::after { /* Растягиваем хвостик между двумя якорями */ top: anchor(--tooltip-el end); bottom: anchor(--button-el start); translate: 0 -7px; rotate: 45deg;}
/* Для вертикальной ориентации */ .tooltip::before, .tooltip::after { left: anchor(--button-el start); right: anchor(--button-el end); width: 10px; max-height: 10px; } /* Для случая, когда тултип под кнопкой */ .tooltip::before { /* Растягиваем хвостик между двумя якорями */ top: anchor(--button-el end); bottom: anchor(--tooltip-el start); translate: 0 7px; rotate: 45deg; } /* Для случая, когда тултип над кнопкой */ .tooltip::after { /* Растягиваем хвостик между двумя якорями */ top: anchor(--tooltip-el end); bottom: anchor(--button-el start); translate: 0 -7px; rotate: 45deg; }
Хвостики для горизонтальной ориентации опишем с помощью :
и :
у элемента .tooltip
.
/* Для горизонтальной ориентации */.tooltip .tooltip-content::before,.tooltip .tooltip-content::after { top: anchor(--button-el start); bottom: anchor(--button-el end); height: 10px; max-width: 10px;}/* Для случая, когда тултип справа от кнопки */.tooltip .tooltip-content::before { /* Растягиваем хвостик между двумя якорями */ left: anchor(--button-el end); right: anchor(--tooltip-el start); translate: 7px; rotate: 45deg;}/* Для случая, когда тултип слева от кнопки */.tooltip .tooltip-content::after { /* Растягиваем хвостик между двумя якорями */ left: anchor(--tooltip-el end); right: anchor(--button-el start); translate: -7px; rotate: 45deg;}
/* Для горизонтальной ориентации */ .tooltip .tooltip-content::before, .tooltip .tooltip-content::after { top: anchor(--button-el start); bottom: anchor(--button-el end); height: 10px; max-width: 10px; } /* Для случая, когда тултип справа от кнопки */ .tooltip .tooltip-content::before { /* Растягиваем хвостик между двумя якорями */ left: anchor(--button-el end); right: anchor(--tooltip-el start); translate: 7px; rotate: 45deg; } /* Для случая, когда тултип слева от кнопки */ .tooltip .tooltip-content::after { /* Растягиваем хвостик между двумя якорями */ left: anchor(--tooltip-el end); right: anchor(--button-el start); translate: -7px; rotate: 45deg; }
JavaScript
СкопированоДля начала найдём все элементы, которые понадобятся нам для работы с тултипом: сам тултип и кнопку, к которой он привязан.
const tooltip = document.querySelector('#tooltip')const tooltipAnchor = document.querySelector('#tooltip-anchor')
const tooltip = document.querySelector('#tooltip') const tooltipAnchor = document.querySelector('#tooltip-anchor')
Напишем функции для отображения и скрытия тултипа. Ранее мы это делали с помощью смены CSS классов. Теперь используем методы поповера.
const showTooltip = () => { tooltip.showPopover()}const hideTooltip = () => { tooltip.hidePopover()}
const showTooltip = () => { tooltip.showPopover() } const hideTooltip = () => { tooltip.hidePopover() }
Навесим соответствующие обработчики событий на кнопку. Она, как и в предыдущих репептах, умеет показывать тултип на наведение мышью или фокусе с клавиатуры. Закрываться тултип будет при потере кнопкой ховера или фокуса, а также при нажатии на клавишу Escape.
tooltipAnchor.addEventListener('mouseenter', showTooltip)tooltipAnchor.addEventListener('focus', showTooltip)tooltipAnchor.addEventListener('mouseleave', hideTooltip)tooltipAnchor.addEventListener('blur', hideTooltip)tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() }})
tooltipAnchor.addEventListener('mouseenter', showTooltip) tooltipAnchor.addEventListener('focus', showTooltip) tooltipAnchor.addEventListener('mouseleave', hideTooltip) tooltipAnchor.addEventListener('blur', hideTooltip) tooltipAnchor.addEventListener('keydown', (event) => { if (event.key === 'Escape') { hideTooltip() } })
Выводы
СкопированоВ зависимости от требований можно использовать как статичный, так и адаптивный тултипы.
Если интерфейс не имеет скролла, то можно использовать статичный.
В остальных случаях лучше подойдёт адаптивный.
Если браузерная поддержка позволяет, то адаптивность можно реализовать с помощью CSS Anchor Positioning. Иначе придется использовать Intersection Observer.