Всплывающие подсказки (CSS)

Стандартную всплывающую подсказку в браузере иногда требуется заменить более презентабельным, а главное кроссбраузерным с точки зрения визуального представления решением. Не секрет, что простой и красивый tooltip можно реализовать исключительно средствами CSS.

Как работает tooltip?

Всплывающая подсказка представляет собой альтернативу атрибута title и отображается благодаря псевдоэлементам ::before и ::after с абсолютным позиционированием и функции attr(), которая получает текст подсказки из атрибута data-*. Поэтому для её правильного отображения родительский элемент должен иметь position: relative. Ввиду простоты реализации предлагаемый tooltip содержит небольшие ограничения:

  1. Подсказка не может выводить HTML, только обычный текст;
  2. Во избежание конфликта стилей элемент, к которому добавляется tooltip, не должен позиционироваться иначе, чем относительно своего исходного места, и не должен содержать упомянутые выше псевдоэлементы;
  3. На маленьких разрешениях экрана за счет абсолютного позиционирования подсказка может выходить за пределы области просмотра, если она содержит слишком длинный контент. Об этом стоит позаботиться в @media-запросах.

Под капотом: HTML и (S)CSS

Для отображения подсказки к блочному или строчному элементу достаточно добавить атрибут data-tip, поэтому разметка HTML выглядит крайне тривиально:

<span data-tip="Всплывающий текст">Some text</span> <!-- подсказка появится сверху элемента-->

В SCSS для удобства добавлено несколько переменных, которые можно настроить заранее, — это шрифт, цвет, эффект перехода и возможность сменить название атрибута:

$tip-font: 400 100%/1 Ubuntu, serif; // значение свойства font в CSS
$tip-color: ( bg: rgba(63, 63, 63, .9), text: rgba(255, 255, 255, .9), arrow: rgba(63, 63, 63, .9) ); // отображаемые цвета фона, текста и стрелки
$tip-transition: .1s ease-out;  // transition-duration и transition-timing-function для opacity и visibility 
$tip-data-name: tip; // если необходимо использовать для подсказки другой атрибут data-* 

[data-#{$tip-data-name}] {

  position: relative;
  outline: 0;
  
  &::before,
  &::after {
    transition: visibility $tip-transition, opacity $tip-transition;
    -webkit-transition: visibility $tip-transition, opacity $tip-transition;
    opacity: 0;
    visibility: hidden;
    z-index: 99;
    transform: translateX(-50%);
    bottom: calc(100% + .2em);
    bottom: -webkit-calc(100% + .2em);
    left: 50%;
    position: absolute;
    display: block;
    pointer-events: none;
  }

  &::before {
    margin-bottom: .25em;
    padding: .475em .5em;
    content: attr(data-#{$tip-data-name});
    font: $tip-font;
    background-color: map-get($tip-color,bg);
    color: map-get($tip-color,text);
    letter-spacing: 0;
    word-spacing: normal;
    font-weight: normal;
    font-style: normal;
    white-space: pre-wrap;
    max-width: 33.3vw; // подсказка не растянется больше, чем на треть viewport
    text-align: center;
    width: intrinsic;
    width: max-content;
    width: -moz-max-content;
    width: -webkit-max-content;
  }
  
  &::after {
    content: "";
    width: 0;
    height: 0;
    border-style: solid;
    border-color: transparent;
    border-width: .35em .3em 0 .3em;
    border-top-color:  map-get($tip-color,arrow);
  }

  &:hover {
    &::after,
    &::before {
      visibility: visible;
      opacity: 1;
      transition-delay: .1s;
      -webkit-transition-delay: .1s;
      transition-duration: .2s;
      -webkit-transition-duration: .2s;
    }
  }

}

Если внимательно проанализировать код, можно заметить правила width: max-content и white-space: pre-wrap. Они необходимы, чтобы подсказка растягивалась вне зависимости от ширины родительского элемента, но при этом могла быть многострочной, а благодаря max-width: 33.3vw её максимальный размер не превышал трети текущего viewport.

Проблемы кроссбраузерности

Даже у такого незамысловатого способа реализации всплывающих подсказок есть некоторые проблемы с кроссбраузерностью, и речь идет вовсе не о древних браузерах. Так, например, Firefox испытывает явные трудности с определением размера строчного элемента, к которому добавляется псевдоэлемент, в результате чего позиционирование рассчитывается неверно и может «перескочить» на предыдущую строку:

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

Предотвратить недоразумение поможет правило display: inline-block для элемента, к которому добавляется tooltip. Однако это в свою очередь приведет к увеличению области его содержимого по вертикали и, следовательно, возрастёт расстояние между элементом и псевдоэлементами, поэтому целесообразно сократить отступ bottom. С блочными элементами подобной проблемы не возникает:

@mixin ff-fix() {
  p {
    [data-#{$tip-data-name}] {
      display: inline-block !important;
      &::before, &::after {
        bottom: calc(100% - .25em);
      }
    }
  }
}

@media (min--moz-device-pixel-ratio:0) {
  @include ff-fix()
}

@media (-moz-touch-enabled: 1) { // firefox mobile
   @include ff-fix();
}

Фактически, правило display: inline-block может быть применено к подсказкам абсолютно для всех браузеров, однако следует учитывать потенциальный конфликт стилей, если подсказка будет применяться к элементу с блочным, табличным и др. значением display.

Другая менее значимая неприятность заключается в особенностях рендеринга текста, к которому была применена трансформация. Почти во всех браузерах наблюдается едва заметное изменение гарнитуры шрифта, приводящее к появлению и последующему удалению лишнего пикселя ширины, что создаёт эффект «мерцания» всего псевдоэлемента с текстом. Для устранения этого следует добавить дополнительную 3d-транформацию — так называемый хак для производительности и ещё пару полезных свойств:

[data-#{$tip-data-name}] {
  &::before,
  &::after {
    transform: translateX(-50%) translateZ(0);
    -webkit-font-smoothing: subpixel-antialiased;
    backface-visibility: hidden;
  }
}

По мимо прочего не все браузеры поддерживают относительные единицы измерения vh, vw, vmin, vmax и значения max-content или min-content для ширины элемента. В последнем случае это касается всех Internet Explorer и Edge, поэтому следует использовать строчную сетку, с помощью которой подсказка будет растягиваться по ширине контента, однако способа ограничить это максимальным значением не нашлось:

@mixin ie-fix() {
  [data-#{$tip-data-name}] {
    &::before {
      display: -ms-inline-grid;
      -ms-grid-columns: max-content;
    }
  }
}

@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { // MS IE 10-11
  @include ie-fix();
}

@supports (-ms-ime-align:auto) { // MS EDGE
  @include ie-fix();
}

Пример всплывающих подсказок

Собрав воедино весь SCSS получается удобная адаптивная всплывающая подсказка:

Пара фишек в заключение

1. Содержимое tooltip может быть растянуто на несколько строк, для чего необходимо разделять текст в атрибуте символами перевода строки и возврата каретки — &#13;&#10:

<span data-tip="Многострочный текст&#13;&#10nдля всплывающей подсказки">Some text</span>

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

[data-#{$tip-data-name}] {
  border-bottom: 1px dotted currentColor;
  cursor: help;
}

Чтобы подсказка не выходила за viewport на небольших экранах из-за абсолютного позиционирования, для неё стоит уменьшить шрифт, тогда сократится и область горизонтального содержимого:

@media screen and (max-width: 480px) {
  [data-#{$tip-data-name}] {
    &::before {
      font-size: 75%;
    }
  }
}