Привязывайте элементы друг к другу с помощью позиционирования привязки CSS

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

<!-- index.html -->
<div class="container">
  <a href="/link" class="anchor">I’m the anchor</a>
  <div class="anchored">I’m the anchored thing</div>
</div>
/* styles.css */
.container {
  position: relative;
}
.anchored {
  position: absolute;
}

Эти решения часто не идеальны. Им нужен JavaScript или дополнительная разметка. API позиционирования привязки CSS призван решить эту проблему, предоставляя API CSS для привязки элементов. Он предоставляет средства для позиционирования и размера одного элемента в зависимости от положения и размера других элементов.

На изображении показан макет окна браузера с подробным описанием структуры всплывающей подсказки.

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

Вы можете опробовать API позиционирования привязки CSS в Chrome Canary под флагом «Экспериментальные функции веб-платформы». Чтобы включить этот флаг, откройте Chrome Canary и посетите chrome://flags . Затем включите флаг «Экспериментальные функции веб-платформы».

Команда Oddbird также разрабатывает полифилл . Обязательно проверьте репозиторий на github.com/oddbird/css-anchor-positioning .

Проверить поддержку привязки можно с помощью:

@supports(anchor-name: --foo) {
  /* Styles... */
}

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

Проблема

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

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

Текущие решения

В настоящее время существует несколько различных способов решения этой проблемы.

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

<div class="containing-block">
  <div class="tooltip">Anchor me!</div>
  <a class="anchor">The anchor</a>
</div>
.containing-block {
  position: relative;
}

.tooltip {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
}

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

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

<div class="tooltip">Anchor me!</div>
<a class="anchor">The anchor</a>
:root {
  --anchor-width: 120px;
  --anchor-top: 40vh;
  --anchor-left: 20vmin;
}

.anchor {
  position: absolute;
  top: var(--anchor-top);
  left: var(--anchor-left);
  width: var(--anchor-width);
}

.tooltip {
  position: absolute;
  top: calc(var(--anchor-top));
  left: calc((var(--anchor-width) * 0.5) + var(--anchor-left));
  transform: translate(-50%, calc(-100% - 10px));
}

Но что, если вы не знаете положение своего якоря? Вероятно, вам придется вмешаться с помощью JavaScript. Вы могли бы сделать что-то вроде следующего кода, но теперь это означает, что ваши стили начинают просачиваться из CSS в JavaScript.

const setAnchorPosition = (anchored, anchor) => {
  const bounds = anchor.getBoundingClientRect().toJSON();
  for (const [key, value] of Object.entries(bounds)) {
    anchored.style.setProperty(`--${key}`, value);
  }
};

const update = () => {
  setAnchorPosition(
    document.querySelector('.tooltip'),
    document.querySelector('.anchor')
  );
};

window.addEventListener('resize', update);
document.addEventListener('DOMContentLoaded', update);

Это начинает вызывать некоторые вопросы:

  • Когда мне рассчитывать стили?
  • Как рассчитать стили?
  • Как часто я рассчитываю стили?

Это решает проблему? Это может подойти для вашего случая использования, но есть одна проблема: наше решение не адаптируется. Это не отзывчиво. Что, если мой привязанный элемент будет обрезан областью просмотра?

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

Чтобы облегчить эту боль, вы можете воспользоваться решением на JavaScript, которое поможет вам. Это повлечет за собой затраты на добавление зависимости в ваш проект и может привести к проблемам с производительностью в зависимости от того, как вы их используете. Например, некоторые пакеты используют requestAnimationFrame для сохранения правильной позиции. Это означает, что вам и вашей команде необходимо ознакомиться с пакетом и параметрами его конфигурации. В результате ваши вопросы и решения могут не сократиться, а измениться. Это часть «почему» для позиционирования привязки CSS. Это отвлечет вас от размышлений о проблемах производительности при вычислении позиции.

Вот как может выглядеть код использования « floating-ui », популярного пакета для решения этой проблемы:

import {computePosition, flip, offset, autoUpdate} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.1/+esm';

const anchor = document.querySelector('.anchor')
const tooltip = document.querySelector('.tooltip')

const updatePosition = () => {  
  computePosition(anchor, tooltip, {
    placement: 'top',
    middleware: [offset(10), flip()]
  })
    .then(({x, y}) => {
      Object.assign(tooltip.style, {
        left: `${x}px`,
        top: `${y}px`
      })
  })
};

const clean = autoUpdate(anchor, tooltip, updatePosition);

Попробуйте изменить положение привязки в этой демонстрации, в которой используется этот код.

«Подсказка» может вести себя не так, как вы ожидаете. Он реагирует на выход за пределы области просмотра по оси Y, но не по оси X. Покопайтесь в документации , и вы, скорее всего, найдете решение, которое подойдет именно вам.

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

Использование позиционирования якоря

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

  • JavaScript не требуется.
  • Пусть браузер выберет лучшую позицию под вашим руководством.
  • Больше никаких сторонних зависимостей
  • Никаких элементов-оберток.
  • Работает с элементами, которые находятся в верхнем слое.

Давайте воссоздадим и решим проблему, которую мы пытались решить выше. Но вместо этого используйте аналогию с лодкой с якорем. Они представляют закрепленный элемент и якорь. Вода представляет собой содержащий блок.

Во-первых, вам нужно выбрать, как определить якорь. Вы можете сделать это в своем CSS, установив свойство anchor-name в элементе привязки. Он принимает значение пунктирного идентификатора .

.anchor {
  anchor-name: --my-anchor;
}

Альтернативно вы сможете определить привязку в своем HTML с помощью атрибута anchor . Значением атрибута является идентификатор элемента привязки. Это создает неявный якорь.

<a id="my-anchor" class="anchor"></a>
<div anchor="my-anchor" class="boat">I’m a boat!</div>

После того как вы определили якорь, вы можете использовать функцию anchor . Функция anchor принимает 3 аргумента:

  • Элемент привязки: anchor-name , которую нужно использовать, или вы можете опустить значение, чтобы использовать implicit привязку. Его можно определить через отношение HTML или с помощью свойства anchor-default со значением anchor-name .
  • Сторона привязки: ключевое слово позиции, которую вы хотите использовать. Это может быть top , right , bottom , left , center и т. д. Или вы можете передать процент. Например, 50% будет равно center .
  • Резервный вариант: это необязательное резервное значение, которое принимает длину или процент.

Вы используете функцию anchor как значение для свойств вставки ( top , right , bottom , left или их логические эквиваленты) привязанного элемента. Вы также можете использовать функцию anchor в calc :

.boat {
  bottom: anchor(--my-anchor top);
  left: calc(anchor(--my-anchor center) - (var(--boat-size) * 0.5));
}

 /* alternative with anchor-default */
.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: calc(anchor(center) - (var(--boat-size) * 0.5));
}

Свойства center вставки нет, поэтому один из вариантов — использовать calc если вы знаете размер привязанного элемента. Почему бы не использовать translate ? Вы можете использовать это:

.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
}

Но браузер не учитывает преобразованные позиции привязанных элементов. Станет понятно, почему это важно при рассмотрении резервных позиций и автоматического позиционирования.

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

.boat {
  width: calc(4 * anchor-size(--my-anchor width));
}

У вас также есть доступ к высоте с помощью anchor-size(--my-anchor height) . И вы можете использовать его для установки размера любой оси или обеих.

Что делать, если вы хотите привязаться к элементу с absolute позиционированием? Правило состоит в том, что элементы не могут быть братьями и сестрами. В этом случае вы можете обернуть привязку контейнером с relative позиционированием. Тогда вы сможете закрепиться на нем.

<div class="anchor-wrapper">
  <a id="my-anchor" class="anchor"></a>
</div>
<div class="boat">I’m a boat!</div>

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

Отслеживание положения прокрутки

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

.boat { anchor-scroll: --my-anchor; }

Попробуйте эту демонстрацию, где вы можете включать и выключать anchor-scroll с помощью флажка в углу.

Однако аналогия здесь несколько неуместна, поскольку в идеальном мире ваша лодка и якорь находятся в воде. Кроме того, такие функции, как Popover API, позволяют держать связанные элементы рядом. Однако позиционирование привязки будет работать с элементами, которые находятся в верхнем слое. Это одно из основных преимуществ API: возможность связывать элементы в разных потоках.

Рассмотрим эту демонстрацию, в которой есть контейнер прокрутки с привязками и всплывающими подсказками. Элементы всплывающей подсказки, являющиеся всплывающими окнами, могут не располагаться рядом с якорями:

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

Возврат позиции и автоматическое позиционирование

Именно здесь сила позиционирования якоря возрастает на новый уровень. position-fallback может позиционировать закрепленный элемент на основе предоставленного вами набора резервных вариантов. Вы управляете браузером своими стилями и позволяете ему определить позицию за вас.

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

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

Прежде чем создавать явный position-fallback , позиционирование привязки также будет предлагать автоматическое позиционирование . Вы можете получить этот переворот бесплатно, используя значение auto как в функции привязки, так и в противоположном свойстве inset. Например, если вы используете anchor для bottom , установите для top значение auto .

.tooltip {
  position: absolute;
  bottom: anchor(--my-anchor auto);
  top: auto;
}

Альтернативой автоматическому позиционированию является использование явного position-fallback . Для этого вам необходимо определить набор резервных позиций. Браузер будет просматривать их, пока не найдет тот, который можно использовать, а затем применит это позиционирование. Если он не может найти тот, который работает, по умолчанию используется первый определенный.

position-fallback которая пытается отобразить всплывающие подсказки сверху, а затем снизу, может выглядеть так:

@position-fallback --top-to-bottom {
  @try {
    bottom: anchor(top);
    left: anchor(center);
  }

  @try {
    top: anchor(bottom);
    left: anchor(center);
  }
}

Применение этого к всплывающим подсказкам выглядит следующим образом:

.tooltip {
  anchor-default: --my-anchor;
  position-fallback: --top-to-bottom;
}

Использование anchor-default означает, что вы можете повторно использовать position-fallback для других элементов. Вы также можете использовать настраиваемое свойство с областью действия, чтобы установить anchor-default .

Рассмотрим эту демонстрацию еще раз с использованием лодки. Имеется набор position-fallback . Когда вы измените положение якоря, лодка приспособится оставаться внутри контейнера. Попробуйте также изменить значение заполнения, которое регулирует отступы тела. Обратите внимание, как браузер корректирует позиционирование. Позиции изменяются путем изменения выравнивания сетки контейнера.

На этот раз position-fallback более подробный, пробуя позиции по часовой стрелке.

.boat {
  anchor-default: --my-anchor;
  position-fallback: --compass;
}

@position-fallback --compass {
  @try {
    bottom: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    right: anchor(left);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }
}


Примеры

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

Контекстные меню

Начнем с контекстного меню с использованием API Popover. Идея состоит в том, что нажатие кнопки с шевроном откроет контекстное меню. И это меню будет иметь собственное меню, которое можно будет расширить.

Разметка здесь не играет важной роли. Но у вас есть три кнопки, каждая из которых использует popovertarget . Затем у вас есть три элемента, использующих атрибут popover . Это дает вам возможность открывать контекстные меню без использования JavaScript. Это может выглядеть так:

<button popovertarget="context">
  Toggle Menu
</button>        
<div popover="auto" id="context">
  <ul>
    <li><button>Save to your Liked Songs</button></li>
    <li>
      <button popovertarget="playlist">
        Add to Playlist
      </button>
    </li>
    <li>
      <button popovertarget="share">
        Share
      </button>
    </li>
  </ul>
</div>
<div popover="auto" id="share">...</div>
<div popover="auto" id="playlist">...</div>

Теперь вы можете определить position-fallback и поделиться ею между контекстными меню. Мы также обязательно отключили все inset стилей для всплывающих окон.

[popovertarget="share"] {
  anchor-name: --share;
}

[popovertarget="playlist"] {
  anchor-name: --playlist;
}

[popovertarget="context"] {
  anchor-name: --context;
}

#share {
  anchor-default: --share;
  position-fallback: --aligned;
}

#playlist {
  anchor-default: --playlist;
  position-fallback: --aligned;
}

#context {
  anchor-default: --context;
  position-fallback: --flip;
}

@position-fallback --aligned {
  @try {
    top: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }

  @try {
    top: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(bottom);
    left: anchor(right);
  }

  @try {
    right: anchor(left);
    bottom: anchor(bottom);
  }
}

@position-fallback --flip {
  @try {
    bottom: anchor(top);
    left: anchor(left);
  }

  @try {
    right: anchor(right);
    bottom: anchor(top);
  }

  @try {
    top: anchor(bottom);
    left: anchor(left);
  }

  @try {
    top: anchor(bottom);
    right: anchor(right);
  }
}

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

Сосредоточьтесь и следуйте

Эта демонстрация объединяет примитивы CSS, добавляя :has() . Идея состоит в том, чтобы перенести визуальный индикатор для input , находящегося в фокусе.

Сделайте это, установив новую привязку во время выполнения. В этой демонстрации настраиваемое свойство с областью действия обновляется при фокусе ввода.

#email {
    anchor-name: --email;
  }
  #name {
    anchor-name: --name;
  }
  #password {
    anchor-name: --password;
  }
:root:has(#email:focus) {
    --active-anchor: --email;
  }
  :root:has(#name:focus) {
    --active-anchor: --name;
  }
  :root:has(#password:focus) {
    --active-anchor: --password;
  }

:root {
    --active-anchor: --name;
    --active-left: anchor(var(--active-anchor) right);
    --active-top: calc(
      anchor(var(--active-anchor) top) +
        (
          (
              anchor(var(--active-anchor) bottom) -
                anchor(var(--active-anchor) top)
            ) * 0.5
        )
    );
  }
.form-indicator {
    left: var(--active-left);
    top: var(--active-top);
    transition: all 0.2s;
}

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

Расчет гистограммы

Еще одна интересная вещь, которую вы можете сделать с позиционированием привязки, — это объединить его с calc . Представьте себе диаграмму, в которой есть всплывающие окна, аннотирующие диаграмму.

Вы можете отслеживать самые высокие и самые низкие значения, используя CSS min и max . CSS для этого может выглядеть примерно так:

.chart__tooltip--max {
    left: anchor(--chart right);
    bottom: max(
      anchor(--anchor-1 top),
      anchor(--anchor-2 top),
      anchor(--anchor-3 top)
    );
    translate: 0 50%;
  }

Есть некоторый JavaScript для обновления значений диаграммы и немного CSS для стилизации диаграммы. Но позиционирование привязки позаботится об обновлениях макета за нас.

Изменение размера маркеров

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

Вы можете относиться к опорным точкам как к пользовательским маркерам изменения размера и опираться на inset значение.

.container {
   position: absolute;
   inset:
     anchor(--handle-1 top)
     anchor(--handle-2 right)
     anchor(--handle-2 bottom)
     anchor(--handle-1 left);
 }

В этой демонстрации GreenSock Draggable делает маркеры перетаскиваемыми. Но размер элемента <img> изменяется, чтобы заполнить контейнер, который приспосабливается к заполнению промежутка между маркерами.

Меню выбора?

Последнее — своего рода намек на то, что будет дальше. Но вы можете создать фокусируемый поповер, и теперь у вас есть позиционирование привязки. Вы можете создать основу для стилизованного элемента <select> .

<div class="select-menu">
<button popovertarget="listbox">
 Select option
 <svg>...</svg>
</button>
<div popover="auto" id="listbox">
   <option>A</option>
   <option>Styled</option>
   <option>Select</option>
</div>
</div>

Неявный anchor облегчит эту задачу. Но CSS для элементарной отправной точки может выглядеть так:

[popovertarget] {
 anchor-name: --select-button;
}
[popover] {
  anchor-default: --select-button;
  top: anchor(bottom);
  width: anchor-size(width);
  left: anchor(left);
}

Объедините функции Popover API с позиционированием привязки CSS, и вы уже почти готовы.

Это здорово, когда вы начинаете вводить такие вещи, как :has() . Вы можете повернуть маркер при открытии:

.select-menu:has(:open) svg {
  rotate: 180deg;
}

Где бы вы могли взять его в следующий раз? Что еще нам нужно, чтобы сделать этот select функционирующим? Мы оставим это для следующей статьи. Но не волнуйтесь, скоро появятся стилизованные элементы выбора. Следите за обновлениями!


Вот и все!

Веб-платформа развивается. Позиционирование привязки CSS — важная часть улучшения разработки элементов управления пользовательского интерфейса. Это отвлечет вас от некоторых сложных решений. Но это также позволит вам делать то, что вы никогда не могли сделать раньше. Например, стилизация элемента <select> ! Поделитесь с нами вашими мыслями.

Фото CHUTTERSNAP на Unsplash