Создание компонента всплывающей подсказки

Базовый обзор того, как создать адаптивный к цвету и доступный пользовательский элемент подсказки.

В этом посте я хочу поделиться своими мыслями о том, как создать адаптивный к цвету и доступный пользовательский элемент <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>

Теперь для программ чтения с экрана это распознается как всплывающая подсказка. Посмотрите в следующем примере, как первый элемент ссылки имеет распознанный элемент всплывающей подсказки в своем дереве, а второй — нет? У второго нет этой роли. В разделе стилей мы улучшим это древовидное представление.

Снимок экрана дерева доступности Chrome DevTools, представляющего HTML. Показывает ссылку с текстом 'top; Имеет подсказку: Эй, подсказку!' это можно сфокусировать. Внутри него находится статический текст «top» и элемент подсказки.

Далее нам нужно, чтобы всплывающая подсказка не была фокусируемой. Если программа чтения с экрана не понимает роль всплывающей подсказки, она позволит пользователям сфокусировать <tool-tip> на чтении содержимого, а для пользовательского опыта это не требуется. Программы чтения с экрана добавит содержимое к родительскому элементу, и поэтому для его доступности не требуется фокус. Здесь мы можем использовать inert чтобы гарантировать, что ни один пользователь случайно не обнаружит это содержимое всплывающей подсказки в своей вкладке:

<tool-tip inert role="tooltip">A tooltip</tool-tip>

Еще один скриншот дерева доступности Chrome DevTools, на этот раз элемент всплывающей подсказки отсутствует.

Затем я решил использовать атрибуты в качестве интерфейса для указания положения всплывающей подсказки. По умолчанию все <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>

Скриншот изображения с подсказкой «Логотип черепа The GUI Challenges».

Здесь я помещаю <tool-tip> внутри элемента <abbr> :

<p>
  The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>

Скриншот абзаца с подчеркнутой аббревиатурой HTML и всплывающей подсказкой над ним с надписью «Язык гипертекстовой разметки».

Доступность

Поскольку я решил создавать всплывающие подсказки, а не переключатели, этот раздел стал намного проще. Во-первых, позвольте мне обрисовать желаемый пользовательский опыт:

  1. В ограниченном пространстве или загроможденных интерфейсах скрывайте дополнительные сообщения.
  2. Когда пользователь наводит курсор, фокусируется или использует прикосновение для взаимодействия с элементом, покажите сообщение.
  3. Когда наведение, фокус или касание завершаются, снова скройте сообщение.
  4. Наконец, убедитесь, что любое движение уменьшено, если пользователь указал предпочтение уменьшению движения.

Наша цель — предоставление дополнительных сообщений по требованию. Зрячий пользователь мыши или клавиатуры может навести курсор, чтобы увидеть сообщение и прочитать его глазами. Незрячий пользователь программы чтения с экрана может сфокусироваться, чтобы просмотреть сообщение, получив его вслух через свой инструмент.

Снимок экрана: MacOS VoiceOver читает ссылку со всплывающей подсказкой

В предыдущем разделе мы рассмотрели дерево доступности, роль всплывающей подсказки и инертность, осталось протестировать его и убедиться, что пользовательский опыт правильно отображает сообщение всплывающей подсказки пользователю. После тестирования неясно, какая часть звукового сообщения является всплывающей подсказкой. Это можно увидеть и во время отладки в дереве доступности: текст ссылки «top» без колебаний сочетается с «Смотрите, всплывающие подсказки!». Средство чтения с экрана не разбивает текст и не идентифицирует его как содержимое всплывающей подсказки.

Снимок экрана дерева специальных возможностей Chrome DevTools, где текст ссылки гласит: «Эй, всплывающая подсказка!».

Добавьте псевдоэлемент только для чтения с экрана в <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;
}

Ниже вы можете увидеть обновленное дерево специальных возможностей, в котором после текста ссылки теперь есть точка с запятой и подсказка «Есть подсказка:».

Обновленный снимок экрана дерева специальных возможностей Chrome DevTools, где текст ссылки имеет улучшенную формулировку: 'top ; Имеет подсказку: Эй, подсказку!».

Теперь, когда пользователь программы чтения с экрана фокусирует ссылку, он говорит «сверху» и делает небольшую паузу, а затем объявляет «есть подсказка: посмотрите, подсказки». Это дает пользователю программы чтения с экрана пару полезных советов по UX. Задержка обеспечивает хорошее разделение между текстом ссылки и всплывающей подсказкой. Кроме того, когда объявляется «подсказка есть», пользователь программы чтения с экрана может легко отменить ее, если он уже слышал ее раньше. Это очень напоминает быстрое наведение и снятие наведения, как вы уже видели дополнительное сообщение. Это было похоже на хороший UX-паритет.

Стили

Элемент <tool-tip> будет дочерним элементом элемента, для которого он представляет дополнительные сообщения, поэтому давайте сначала начнем с основ эффекта наложения. Уберите его из потока документов с position absolute :

tool-tip {
  position: absolute;
  z-index: 1;
}

Если родительский контекст не является контекстом стека, всплывающая подсказка будет располагаться к ближайшему контексту, а это не то, что нам нужно. В блоке есть новый селектор, который может помочь :has() :

Поддержка браузера

  • Хром: 105.
  • Край: 105.
  • Фаерфокс: 121.
  • Сафари: 15.4.

Источник

: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 , содержащую родительские элементы, осведомленной о взаимодействии пользователя, чтобы переключить видимость дочерней всплывающей подсказки. Пользователи мыши могут наводить курсор, пользователи клавиатуры и программы чтения с экрана — фокусироваться, а пользователи сенсорного экрана — касаться.

Поскольку наложение «Показать и скрыть» работает для зрячих пользователей, пришло время добавить несколько стилей для оформления тем, позиционирования и добавления формы треугольника к пузырю. Следующие стили начинают использовать пользовательские свойства, опираясь на то, где мы находимся на данный момент, но также добавляя тени, типографику и цвета, чтобы они выглядели как плавающая всплывающая подсказка:

Скриншот всплывающей подсказки в темном режиме, плавающей над ссылкой «block-start».

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 для лучшего позиционирования объектов в окне. А пока я буду делать подсказки.

Давайте разнообразим наши подходы и изучим все способы разработки в Интернете.

Создайте демо-версию, пришлите мне в Твиттере ссылки, и я добавлю ее в раздел ремиксов сообщества ниже!

Ремиксы сообщества

Здесь пока смотреть нечего.

Ресурсы