Базовый обзор того, как создать адаптивный к цвету и доступный пользовательский элемент подсказки.
В этом посте я хочу поделиться своими мыслями о том, как создать адаптивный к цвету и доступный пользовательский элемент <tool-tip>
. Попробуйте демо-версию и просмотрите исходный код !
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Всплывающая подсказка — это немодальное, неблокирующее и неинтерактивное наложение, содержащее дополнительную информацию для пользовательских интерфейсов. По умолчанию он скрыт и становится невидимым при наведении курсора мыши или фокусе на связанный элемент. Всплывающую подсказку нельзя выбрать или взаимодействовать с ней напрямую. Всплывающие подсказки не заменяют метки или другую важную информацию. Пользователь должен иметь возможность полностью выполнить свою задачу без подсказки.
Переключить подсказку против подсказки
Как и у многих компонентов, существуют разные описания того, что такое всплывающая подсказка, например, в MDN , WAI ARIA , Sarah Higley и Inclusive Components . Мне нравится разделение между всплывающими подсказками и переключателями. Всплывающая подсказка должна содержать неинтерактивную дополнительную информацию, тогда как подсказка может содержать интерактивную и важную информацию. Основной причиной разделения является доступность: как пользователи должны переходить к всплывающему окну и иметь доступ к информации и кнопкам внутри. Подсказки быстро усложняются.
Вот видео с подсказкой с сайта Designcember ; интерактивное наложение, которое пользователь может открыть и изучить, а затем закрыть с помощью легкого закрытия или клавиши Escape:
Этот вызов GUI пошел по пути всплывающей подсказки, стремясь сделать почти все с помощью 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;
}
Ниже вы можете увидеть обновленное дерево доступности, в котором после текста ссылки теперь есть точка с запятой и подсказка «Есть подсказка:».
Теперь, когда пользователь программы чтения с экрана фокусирует ссылку, он говорит «сверху» и делает небольшую паузу, а затем объявляет «есть подсказка: посмотрите, подсказки». Это дает пользователю программы чтения с экрана пару полезных советов по UX. Задержка обеспечивает хорошее разделение между текстом ссылки и всплывающей подсказкой. Кроме того, когда объявляется «подсказка есть», пользователь программы чтения с экрана может легко отменить ее, если он уже слышал ее раньше. Это очень напоминает быстрое наведение и снятие наведения, как вы уже видели дополнительное сообщение. Это было похоже на хороший 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;
}
}
Обратите внимание, что это установка состояния «выход», так как состояние «вход» находится в 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-индексом, и API anchor
для лучшего позиционирования объектов в окне. А пока я буду делать подсказки.
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете.
Создайте демо, пришлите мне ссылку в Твиттере , и я добавлю ее в раздел ремиксов сообщества ниже!
Ремиксы сообщества
Здесь пока смотреть нечего.
Ресурсы
- Исходный код на Github