Базовый обзор того, как создать адаптивный по цвету и доступный пользовательский элемент подсказки.
В этой публикации я хочу поделиться своими мыслями о том, как создать адаптивный к цвету и доступный элемент <tool-tip>
. Попробуйте демо-версию и посмотрите исходный код !
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Подсказка — это немодальное, неблокирующее и неинтерактивное наложение, содержащее дополнительную информацию для пользовательских интерфейсов. По умолчанию она скрыта и становится видимой при наведении курсора на связанный с ней элемент или фокусе на нём. Подсказку нельзя выбрать или использовать напрямую. Подсказки не заменяют метки или другую важную информацию; пользователь должен иметь возможность полностью выполнить свою задачу без подсказки.

Не стоит полагаться на подсказки вместо меток.
Подсказка-переключатель против подсказки
Как и многие компоненты, существуют различные описания всплывающей подсказки, например, в MDN , WAI ARIA , Sarah Higley и Inclusive Components . Мне нравится разделение между всплывающей подсказкой и подсказкой-переключателем. Подсказка должна содержать неинтерактивную дополнительную информацию, в то время как подсказка-переключатель может содержать интерактивность и важную информацию. Основная причина такого разделения — доступность: как пользователи должны переходить к всплывающему окну и получать доступ к информации и кнопкам внутри. Подсказки-переключатели быстро становятся сложными.
Ниже представлено видео с подсказкой-переключателем с сайта Designcember ; интерактивное наложение, которое пользователь может закрепить в открытом виде и изучить, а затем закрыть с помощью кнопки «Открыть» или клавиши Esc:
Это задание по созданию графического интерфейса пошло по пути подсказки, пытаясь сделать практически все с помощью CSS, и вот как это сделать.
Разметка
Я решил использовать пользовательский элемент <tool-tip>
. Авторам не нужно превращать пользовательские элементы в веб-компоненты, если они этого не хотят. Браузер будет воспринимать <foo-bar>
как <div>
. Пользовательский элемент можно рассматривать как имя класса с меньшей специфичностью. JavaScript не используется.
<tool-tip>A tooltip</tool-tip>
Это похоже на div с текстом внутри. Мы можем подключиться к дереву доступности совместимых программ чтения с экрана, добавив [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
Теперь программы чтения с экрана распознают это как подсказку. Видите в следующем примере, как первый элемент ссылки распознаётся как элемент подсказки в своём дереве, а второй — нет? У второго элемента такой роли нет. В разделе стилей мы улучшим это древовидное представление.
Далее нам нужно сделать подсказку нефокусируемой. Если программа чтения с экрана не понимает роль подсказки, она позволит пользователям фокусироваться на <tool-tip>
для чтения содержимого, что не требуется для пользовательского опыта. Программы чтения с экрана добавят содержимое к родительскому элементу, и, таким образом, фокус для доступа к нему не требуется. Здесь мы можем использовать inert
, чтобы гарантировать, что пользователи случайно не найдут это содержимое подсказки в потоке вкладок:
<tool-tip inert role="tooltip">A tooltip</tool-tip>
Затем я решил использовать атрибуты в качестве интерфейса для указания положения подсказки. По умолчанию все элементы <tool-tip>
занимают позицию «сверху», но её положение можно настроить для элемента, добавив tip-position
:
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>
Я предпочитаю использовать атрибуты вместо классов для таких вещей, чтобы <tool-tip>
не мог занимать несколько позиций одновременно. Может быть только одна позиция или ни одной.
Наконец, разместите элементы <tool-tip>
внутри элемента, для которого вы хотите создать подсказку. Здесь я делюсь alt
текстом со зрячими пользователями, размещая изображение и <tool-tip>
внутри элемента <picture>
:
<picture>
<img alt="The GUI Challenges skull logo" width="100" src="...">
<tool-tip role="tooltip" tip-position="bottom">
The <b>GUI Challenges</b> skull logo
</tool-tip>
</picture>
Здесь я размещаю <tool-tip>
внутри элемента <abbr>
:
<p>
The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>
Доступность
Поскольку я решил создать всплывающие подсказки, а не переключаемые, этот раздел гораздо проще. Для начала позвольте мне обрисовать желаемый пользовательский опыт:
- В ограниченном пространстве или загроможденных интерфейсах скрывайте дополнительные сообщения.
- Когда пользователь наводит курсор, фокусируется или использует прикосновение для взаимодействия с элементом, покажите сообщение.
- После завершения наведения курсора, фокусировки или касания снова скройте сообщение.
- Наконец, убедитесь, что любое движение уменьшено, если пользователь указал предпочтение уменьшению движения.
Наша цель — предоставление дополнительных сообщений по запросу. Зрячий пользователь мыши или клавиатуры может навести курсор, чтобы увидеть сообщение, читая его глазами. Незрячий пользователь программы чтения с экрана может сфокусироваться, чтобы увидеть сообщение, и воспринять его вслух через свой инструмент.

В предыдущем разделе мы рассмотрели дерево доступности, роль подсказок и инертность. Осталось только протестировать его и убедиться, что пользователь правильно отображает подсказку. При тестировании неясно, какая часть звукового сообщения является подсказкой. Это также можно заметить при отладке дерева доступности: текст ссылки «top» без задержек сочетается с фразой «Смотри, подсказки!». Программа чтения с экрана не прерывает текст и не идентифицирует его как содержимое подсказки.
Добавьте псевдоэлемент, предназначенный только для чтения с экрана, к <tool-tip>
, и мы сможем добавить собственный текст подсказки для незрячих пользователей.
&::before {
content: "; Has tooltip: ";
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
Ниже вы можете видеть обновленное дерево доступности, в котором теперь есть точка с запятой после текста ссылки и запрос на подсказку «Has tooltip: ».
Теперь, когда пользователь программы чтения с экрана наводит фокус на ссылку, появляется надпись «top» и небольшая пауза, а затем объявляется «has tooltip: look, tooltips». Это даёт пользователю программы чтения с экрана пару полезных UX-подсказок. Задержка создаёт удобное разделение между текстом ссылки и подсказкой. Кроме того, когда объявляется «has tooltip», пользователь программы чтения с экрана может легко отменить это, если уже слышал это раньше. Это очень напоминает быстрое наведение и снятие курсора, поскольку вы уже видели дополнительное сообщение. Это выглядит как приятное UX-паритет.
Стили
Элемент <tool-tip>
будет дочерним по отношению к элементу, для которого он представляет дополнительное сообщение, поэтому сначала давайте разберёмся с основами эффекта наложения. Выведите его из потока документа с помощью position absolute
:
tool-tip {
position: absolute;
z-index: 1;
}
Если родительский элемент не является контекстом наложения, подсказка будет позиционироваться на ближайшем к нему, что нам не нужно. В блоке появился новый селектор, который может помочь :has()
:
:has(> tool-tip) {
position: relative;
}
Не беспокойтесь слишком сильно о поддержке браузерами. Во-первых, помните, что эти подсказки — вспомогательные. Если они не работают, всё должно быть в порядке. Во-вторых, в разделе JavaScript мы развернем скрипт для реализации полифилла, необходимого для браузеров без поддержки :has()
.
Далее сделаем подсказки неинтерактивными, чтобы они не крали события указателя у своего родительского элемента:
tool-tip {
…
pointer-events: none;
user-select: none;
}
Затем скройте подсказку с помощью непрозрачности, чтобы мы могли перейти к подсказке с помощью плавного перехода:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
и :has()
выполняют основную работу, обеспечивая tool-tip
, содержащую родительские элементы, распознаванием действий пользователя и переключением видимости дочерней подсказки. Пользователи мыши могут наводить курсор, пользователи клавиатуры и экранных дикторов могут фокусироваться, а пользователи сенсорных экранов могут нажимать.
Теперь, когда отображение и скрытие оверлея работает для зрячих пользователей, пришло время добавить стили для оформления, позиционирования и добавления треугольной формы к пузырю. Следующие стили начинают использовать пользовательские свойства, основываясь на текущем состоянии, а также добавляя тени, типографику и цвета, чтобы это выглядело как плавающая подсказка:
tool-tip {
--_p-inline: 1.5ch;
--_p-block: .75ch;
--_triangle-size: 7px;
--_bg: hsl(0 0% 20%);
--_shadow-alpha: 50%;
--_bottom-tip: conic-gradient(from -30deg at bottom, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) bottom / 100% 50% no-repeat;
--_top-tip: conic-gradient(from 150deg at top, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) top / 100% 50% no-repeat;
--_right-tip: conic-gradient(from -120deg at right, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) right / 50% 100% no-repeat;
--_left-tip: conic-gradient(from 60deg at left, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) left / 50% 100% no-repeat;
pointer-events: none;
user-select: none;
opacity: 0;
transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
transition: opacity .2s ease, transform .2s ease;
position: absolute;
z-index: 1;
inline-size: max-content;
max-inline-size: 25ch;
text-align: start;
font-size: 1rem;
font-weight: normal;
line-height: normal;
line-height: initial;
padding: var(--_p-block) var(--_p-inline);
margin: 0;
border-radius: 5px;
background: var(--_bg);
color: CanvasText;
will-change: filter;
filter:
drop-shadow(0 3px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 12px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}
/* create a stacking context for elements with > tool-tips */
:has(> tool-tip) {
position: relative;
}
/* when those parent elements have focus, hover, etc */
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
/* prepend some prose for screen readers only */
tool-tip::before {
content: "; Has tooltip: ";
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
/* tooltip shape is a pseudo element so we can cast a shadow */
tool-tip::after {
content: "";
background: var(--_bg);
position: absolute;
z-index: -1;
inset: 0;
mask: var(--_tip);
}
/* top tooltip styles */
tool-tip:is(
[tip-position="top"],
[tip-position="block-start"],
:not([tip-position]),
[tip-position="bottom"],
[tip-position="block-end"]
) {
text-align: center;
}
Корректировки темы
Подсказка имеет всего несколько цветов, которые нужно настроить, поскольку цвет текста наследуется от страницы через системное ключевое слово CanvasText
. Кроме того, поскольку мы создали пользовательские свойства для хранения значений, мы можем обновлять только эти свойства, а всё остальное пусть делает тема:
@media (prefers-color-scheme: light) {
tool-tip {
--_bg: white;
--_shadow-alpha: 15%;
}
}
Для светлой темы мы адаптируем фон к белому цвету и сделаем тени гораздо менее интенсивными, отрегулировав их непрозрачность.
Справа налево
Для поддержки режимов чтения справа налево пользовательское свойство будет сохранять значение направления документа в значении -1 или 1 соответственно.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
Это можно использовать для облегчения позиционирования подсказки:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
А также помочь определить, где находится треугольник:
tool-tip[tip-position="right"]::after {
--_tip: var(--_left-tip);
}
tool-tip[tip-position="right"]:dir(rtl)::after {
--_tip: var(--_right-tip);
}
Наконец, может также использоваться для логических преобразований в translateX()
:
--_x: calc(var(--isRTL) * -3px * -1);
Позиционирование подсказки
Логически разместите подсказку с помощью свойств inset-block
или inset-inline
, чтобы задать как физическое, так и логическое положение подсказки. Следующий код показывает, как каждое из четырёх положений оформляется для направлений слева направо и справа налево.
Выравнивание верхней части и блока-старта
tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position])) {
inset-inline-start: 50%;
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))::after {
--_tip: var(--_bottom-tip);
inset-block-end: calc(var(--_triangle-size) * -1);
border-block-end: var(--_triangle-size) solid transparent;
}
Выравнивание по правому краю и по краю строки
tool-tip:is([tip-position="right"], [tip-position="inline-end"]) {
inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
tool-tip:is([tip-position="right"], [tip-position="inline-end"])::after {
--_tip: var(--_left-tip);
inset-inline-start: calc(var(--_triangle-size) * -1);
border-inline-start: var(--_triangle-size) solid transparent;
}
tool-tip:is([tip-position="right"], [tip-position="inline-end"]):dir(rtl)::after {
--_tip: var(--_right-tip);
}
Выравнивание дна и торцов блока
tool-tip:is([tip-position="bottom"], [tip-position="block-end"]) {
inset-inline-start: 50%;
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
tool-tip:is([tip-position="bottom"], [tip-position="block-end"])::after {
--_tip: var(--_top-tip);
inset-block-start: calc(var(--_triangle-size) * -1);
border-block-start: var(--_triangle-size) solid transparent;
}
Выравнивание по левому краю и по линейному началу
tool-tip:is([tip-position="left"], [tip-position="inline-start"]) {
inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
tool-tip:is([tip-position="left"], [tip-position="inline-start"])::after {
--_tip: var(--_right-tip);
inset-inline-end: calc(var(--_triangle-size) * -1);
border-inline-end: var(--_triangle-size) solid transparent;
}
tool-tip:is([tip-position="left"], [tip-position="inline-start"]):dir(rtl)::after {
--_tip: var(--_left-tip);
}
Анимация
До сих пор мы только переключали видимость подсказки. В этом разделе мы сначала анимируем непрозрачность для всех пользователей, поскольку это, как правило, безопасный переход с уменьшенным движением. Затем мы анимируем положение преобразования, чтобы подсказка выглядела выезжающей из родительского элемента.
Безопасный и осмысленный переход по умолчанию
Настройте элемент подсказки так, чтобы он менял непрозрачность и трансформировался, как показано ниже:
tool-tip {
opacity: 0;
transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
transition: opacity .2s ease, transform .2s ease;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
Добавление движения к переходу
Для каждой из сторон может появиться подсказка, если пользователь не возражает против движения, слегка измените положение свойства translateX, задав ему небольшое расстояние для перемещения:
@media (prefers-reduced-motion: no-preference) {
:has(> tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_y: 3px;
}
:has(> tool-tip:is([tip-position="right"], [tip-position="inline-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_x: -3px;
}
:has(> tool-tip:is([tip-position="bottom"], [tip-position="block-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_y: -3px;
}
:has(> tool-tip:is([tip-position="left"], [tip-position="inline-start"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_x: 3px;
}
}
Обратите внимание, что это устанавливает состояние «out», поскольку состояние «in» находится в translateX(0)
.
JavaScript
На мой взгляд, JavaScript необязателен. Это связано с тем, что ни одна из этих подсказок не должна быть обязательной для выполнения какой-либо задачи в вашем пользовательском интерфейсе. Поэтому, если подсказки полностью перестанут работать, это не должно стать проблемой. Это также означает, что мы можем рассматривать подсказки как постепенное улучшение. В конечном итоге все браузеры будут поддерживать :has()
, и этот скрипт можно будет полностью убрать.
Скрипт полифилла выполняет две функции и работает только в том случае, если браузер не поддерживает :has()
. Сначала проверьте наличие поддержки :has()
:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Затем найдите родительские элементы <tool-tip>
и дайте им имя класса для работы:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
}
Затем внедрите набор стилей, которые используют это имя класса, имитируя селектор :has()
для точно такого же поведения:
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
let styles = document.createElement('style')
styles.textContent = `
.has_tool-tip {
position: relative;
}
.has_tool-tip:is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
`
document.head.appendChild(styles)
}
Вот и все, теперь все браузеры будут с радостью показывать подсказки, если :has()
не поддерживается.
Заключение
Теперь, когда вы знаете, как я это сделал, как бы вы… 🙂 Я с нетерпением жду API popup
для упрощения работы с подсказками, верхнего слоя , чтобы избежать борьбы за z-index, и API anchor
для лучшего позиционирования элементов в окне. А пока я буду делать подсказки.
Давайте разнообразим наши подходы и изучим все способы развития в Интернете.
Создайте демо, пришлите мне ссылку в Твиттер , и я добавлю ее в раздел ремиксов сообщества ниже!
Ремиксы сообщества
Пока что здесь нечего смотреть.
Ресурсы
- Исходный код на Github