Эта статья — перевод оригинальной статьи «Perfectly Pointed Tooltips: All Four Sides». Это вторая часть, первая часть уже есть на Хабре (Идеально размещённые тултипы: база), будет ещё перевод 3-ей части.

Также я веду телеграм канал «Frontend по‑флотски», где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

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

На момент написания только Chrome и Edge полностью поддерживают возможности, которые мы будем использовать.

Вот демонстрация того, что мы создаём:

https://codepen.io/t_afif/pen/QwyMrvG

Перетащите якорь и посмотрите, как тултип переключается между четырьмя позициями и остаётся по центру относительно якоря.

Первичная настройка

Мы будем использовать ту же структуру кода, что и в первой части. Начнём с тултипа, расположенного над якорем (в позиции «top»).

<div id='anchor'></div>
<div id='tooltip'></div>
#anchor {
  anchor-name: --anchor;
}
#tooltip {
  --d: 1em; /* расстояние между тултипом и якорем */

  position: absolute; 
  position-anchor: --anchor;
  position-area: top;
  bottom: var(--d);
}
https://codepen.io/t_afif/pen/YPwEVqm

Дальше всё будет отличаться от предыдущего примера.

Определение нескольких позиций

Свойство position-try-fallbacks позволяет задавать несколько позиций. Давайте попробуем следующее:

position-try-fallbacks: bottom, left, right;

Не забудем, что размещение связано с блоком-контейнером, который в нашем примере — это body (обозначено пунктирной рамкой):

https://codepen.io/t_afif/pen/LEGQXoG

Мы почти получаем то же поведение, что и в первом примере; однако если вы находитесь близко к правому или левому краю, появляются новые позиции. Вместо того чтобы выходить за границы, браузер переключается на правую или левую позицию.

Как и в первом примере, при переключении на резервные позиции промежуток исчезает. Мы знаем, как это исправить! Вместо явного задания позиций можно воспользоваться функцией «flip».

Чтобы переместить тултип с позиции «top» на «bottom», используем flip-block:

position-try-fallbacks: flip-block, left, right;

Чтобы переместить тултип с позиции «top» на «left», используем flip-start:

position-try-fallbacks: flip-block, flip-start, right; 

Значение flip-block отражает позицию относительно горизонтальной оси, а flip-start делает то же самое по диагонали. С помощью этого значения можно перемещать тултип с «top» на «left» и с «bottom» на «right». Логично, что существует ещё flip-inline, который учитывает вертикальную ось и позволяет перемещать тултип с «left» на «right».

Но как же переместить тултип с позиции «top» на «right»? Нам не хватает ещё одного значения, верно?

Нет, все необходимые значения у нас уже есть. Чтобы переместить тултип с позиции «top» на «right», мы комбинируем два отражения: сначала flip-block, чтобы перейти вниз, а затем flip-start, чтобы сдвинуться вправо:

position-try-fallbacks: flip-block, flip-start, flip-block flip-start;

Или же сначала flip-start, чтобы сдвинуться влево, а затем flip-inline, чтобы перейти вправо:

position-try-fallbacks: flip-block, flip-start, flip-start flip-inline;
https://codepen.io/t_afif/pen/ZYQxKjy

Стоит отметить, что все отражения (flips) учитывают исходную позицию, заданную для элемента, а не предыдущую позицию из position-try-fallbacks или текущую позицию. То есть, если сначала выполнить flip-block, чтобы переместить тултип вниз, то flip-start для второй позиции не будет учитывать нижнюю позицию, а будет исходить из верхней (начальной). Это может запутывать, особенно если позиций много.

Иными словами, браузер сначала преобразует все отражения в позиции (отталкиваясь от исходной позиции), а затем при необходимости выбирает подходящую.

Отключение смещения

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

Для этого мы можем использовать:

justify-self: unsafe anchor-center;

Что это за странное значение!?

После того как мы задали позицию элемента с помощью position-area, мы также можем управлять его выравниванием с помощью justify-self и align-self (или сокращённой записи place-self). Однако обычно значение по умолчанию подходит, и менять его редко приходится.

Для position-area: top выравнивание по умолчанию эквивалентно justify-self: anchor-center и align-self: end.

Разве у нас нет значения «center»? Почему оно называется anchor-center?

Значение center существует, но его поведение отличается от anchor-center. Значение center учитывает центр самой области, а anchor-center — центр якоря по соответствующей оси.

Вот скриншот из моей интерактивной демо-версии, где видно эту разницу:

Кроме того, anchor-center следует логике безопасного выравнивания, что и вызывает поведение смещения. Если места для центрирования недостаточно, элемент сдвигается, чтобы оставаться внутри области контейнера. Чтобы отключить это, мы указываем браузеру использовать «небезопасное» поведение, отсюда и применение:

justify-self: unsafe anchor-center;

Вот демо с только верхней и нижней позициями. Обратите внимание, что тултип теперь выходит за границы слева и справа, вместо того чтобы смещаться.

https://codepen.io/t_afif/pen/Wbrzjjb

А если мы снова добавим левую и правую позиции в резервные, браузер будет использовать их вместо выхода за границы!

https://codepen.io/t_afif/pen/VYeXbVg

Стоит отметить, что justify-self также учитывается при отражении (flip). Это одно из свойств, которое браузер изменяет при переключении позиции. Когда тултип находится в позициях top или bottom, оно остаётся justify-self, а когда в left или right — становится align-self. Ещё одна причина, почему лучше использовать функцию flip, а не задавать позиции явно.

Добавляем min-width

Позиция тултипа теперь корректна, но в некоторых случаях он получается слишком узким.

Это логичное поведение, так как текст внутри может переноситься, чтобы тултип поместился в выбранной позиции. Скорее всего, такое поведение вы захотите оставить, но в нашем случае мы хотим задать min-width, чтобы заставить тултип переключаться на другую позицию до того, как он слишком сожмётся. Также можно использовать max-height по аналогии.

https://codepen.io/t_afif/pen/vELRZKB

Ой, min-width не предотвращает перенос текста, а вместо этого увеличивает высоту! Что?!

Можете догадаться, в чём проблема? Подумайте немного.

Дело в поведении flip.

Все свойства размеров, включая min-width, тоже подвержены отражению. Исходная конфигурация — top, поэтому задание min-width означает, что при выполнении flip-start для перехода на левую или правую позицию оно превращается в min-height, что, конечно, плохо.

Так что вместо этого задаём min-height — при отражении оно превращается в min-width!

Да, но тогда min-height будет применяться к верхней и нижней позициям, что тоже не идеально.

Исправить это можно с помощью кастомных позиций, где мы задаём все свойства вручную.

#tooltip {
  min-width: 10em;

  position-area: top;
  justify-self: unsafe anchor-center;
  bottom: var(--d);
  position-try-fallbacks: flip-block,--left,--right;
}
@position-try --left {
  position-area: left;
  justify-self: normal;
  align-self: unsafe anchor-center;
  right: var(--d);
}
@position-try --right {
  position-area: right;
  justify-self: normal;
  align-self: unsafe anchor-center;
  left: var(--d);
}

Мы используем @position-try, чтобы создать кастомную позицию с заданным именем, и внутри неё определяем все свойства. Вместо использования flip-start для установки левой позиции, я создаю кастомную позицию --left с нужными свойствами, чтобы тултип корректно располагался слева. То же самое делаем для правой позиции. В этом случае min-width сохраняется для всех позиций, так как мы больше не используем flip-start.

Стоит отметить, что при использовании кастомной позиции необходимо убедиться, что вы переопределяете все свойства исходной позиции, заданной на элементе, иначе они всё равно будут применяться. По этой причине я задаю justify-self: normal, чтобы переопределить justify-self: unsafe anchor-center. Значение normal является значением по умолчанию для justify-self.

https://codepen.io/t_afif/pen/MYKGWZX

Хотя это решение работает, оно довольно многословное, поэтому мне стало интересно, можно ли сделать лучше. Оказывается, можно!

Мы можем комбинировать функцию flip и кастомные позиции, чтобы сократить код:

#tooltip {
  position-area: top;
  justify-self: unsafe anchor-center;
  bottom: var(--d);
  position-try: flip-block,--size flip-start,--size flip-start flip-inline;
}
@position-try --size {
  min-height: 12em; /* Это будет min-width! */
}

Когда мы задаём кастомную позицию с flip, браузер сначала берёт свойства, определённые внутри кастомной позиции, а также свойства, уже заданные на элементе, и только потом выполняет отражение. То есть --size flip-start отразит свойства, заданные на элементе, и те, что определены в кастомной позиции --size. В результате min-height превращается в min-width! Умно, правда?

Но ты сказал, что нельзя использовать min-height?

Мы не можем использовать его на основном элементе, так как оно применялось бы к верхней и нижней позициям. Однако внутри кастомной позиции я могу выбрать, где оно применяется, и мне нужно, чтобы оно действовало только для левой и правой позиции. При этом для верхней и нижней позиции никаких ограничений по min-width или min-height не требуется.

https://codepen.io/t_afif/pen/qEbYBge

Теперь позиция нашего тултипа идеальна! Давайте добавим «хвостик».

Добавление хвостика

Сначала создаём форму, которая будет содержать все 4 хвостика.

#tooltip:before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: calc(-1*var(--d));
  clip-path: polygon(
    calc(50% - var(--s)) var(--d),50% .2em,calc(50% + var(--s)) var(--d),
    calc(100% - var(--d)) calc(50% - var(--s)), calc(100% - .2em) 50%,calc(100% - var(--d)) calc(50% + var(--s)),
    calc(50% + var(--s)) calc(100% - var(--d)),50% calc(100% - .2em),calc(50% - var(--s)) calc(100% - var(--d)),
    var(--d) calc(50% + var(--s)), .2em 50%,var(--d) calc(50% - var(--s))
  );
}

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

margin: var(--d);
margin-bottom: 0;

А для остальных сторон ничего делать не нужно! Отражение (flip) выполнит всю работу за нас.

https://codepen.io/t_afif/pen/QwyMrvG

Включите «режим отладки», чтобы увидеть, как форма ведёт себя в каждой позиции.

Заключение

Вторая часть завершена. Теперь вы должны уверенно работать с резервными позициями, функцией flip и кастомными позициями. Если что-то остаётся непонятным, перечитайте статью ещё раз. У нас впереди ещё одно финальное задание, поэтому убедитесь, что всё усвоено, прежде чем переходить к следующей статье.

Комментарии (1)


  1. Kerman
    16.11.2025 19:13

    А так должно быть?