Базовый обзор того, как создать адаптивный к цвету и доступный пользовательский элемент подсказки.
В этом посте я хочу поделиться своими мыслями о том, как создать адаптивный к цвету и доступный пользовательский элемент <tool-tip>
. Попробуйте демо-версию и просмотрите исходный код !
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Всплывающая подсказка — это немодальное, неблокирующее и неинтерактивное наложение, содержащее дополнительную информацию для пользовательских интерфейсов. По умолчанию он скрыт и становится невидимым при наведении курсора мыши или фокусе на связанный элемент. Всплывающую подсказку нельзя выбрать или взаимодействовать с ней напрямую. Всплывающие подсказки не заменяют метки или другую важную информацию. Пользователь должен иметь возможность полностью выполнить свою задачу без подсказки.
Переключить подсказку против подсказки
Like many components, there are varying descriptions of what a tooltip is, for example in MDN , WAI ARIA , Sarah Higley , and 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>
Here I place a <tool-tip>
inside of an <abbr>
element:
<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