Базовый обзор того, как создать компонент вкладок, аналогичный тем, которые есть в приложениях для iOS и Android.
В этом посте я хочу поделиться мыслями о создании веб-компонента «Вкладки», который будет адаптивным, поддерживает ввод с нескольких устройств и работает во всех браузерах. Попробуйте демо .
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Вкладки являются распространенным компонентом дизайн-систем, но могут принимать самые разные формы. Сначала были вкладки рабочего стола, построенные на элементе <frame>
, а теперь у нас есть удобные мобильные компоненты, которые анимируют контент на основе физических свойств. Все они пытаются сделать одно и то же: сэкономить место.
Сегодня основой пользовательского опыта работы с вкладками является область навигации по кнопкам, которая переключает видимость контента в рамке дисплея. Множество различных областей контента занимают одно и то же пространство, но представлены условно в зависимости от кнопки, выбранной в навигации.
Веб-тактика
В целом этот компонент показался мне довольно простым в создании благодаря нескольким важным функциям веб-платформы:
-
scroll-snap-points
для элегантного взаимодействия с помощью смахивания и клавиатуры с соответствующими позициями остановки прокрутки - Глубокие ссылки через хэши URL-адресов для поддержки привязки прокрутки на странице и поддержки совместного использования браузером.
- Поддержка чтения с экрана с разметкой элементов
<a>
иid="#hash"
-
prefers-reduced-motion
для включения плавных переходов и мгновенной прокрутки страницы. - Встроенная веб-функция
@scroll-timeline
для динамического подчеркивания и изменения цвета выбранной вкладки.
HTML
По сути, UX здесь таков: щелкните ссылку, URL-адрес будет представлять состояние вложенной страницы, а затем вы увидите обновление области контента по мере прокрутки браузера к соответствующему элементу.
Здесь есть некоторые элементы структурного контента: ссылки и :target
. Нам нужен список ссылок, для которых отлично подходит <nav>
, и список элементов <article>
, для которых отлично подходит <section>
. Каждый хеш ссылки будет соответствовать разделу, позволяя браузеру прокручивать элементы посредством привязки.
Например, при нажатии на ссылку автоматически фокусируется статья :target
в Chrome 89, JS не требуется. Затем пользователь может, как всегда, прокручивать содержимое статьи с помощью своего устройства ввода. Как указано в разметке, это бесплатный контент.
Для организации вкладок я использовал следующую разметку:
<snap-tabs>
<header>
<nav>
<a></a>
<a></a>
<a></a>
<a></a>
</nav>
</header>
<section>
<article></article>
<article></article>
<article></article>
<article></article>
</section>
</snap-tabs>
Я могу установить связи между элементами <a>
и <article>
с помощью свойств href
и id
следующим образом:
<snap-tabs>
<header>
<nav>
<a href="#responsive"></a>
<a href="#accessible"></a>
<a href="#overscroll"></a>
<a href="#more"></a>
</nav>
</header>
<section>
<article id="responsive"></article>
<article id="accessible"></article>
<article id="overscroll"></article>
<article id="more"></article>
</section>
</snap-tabs>
Затем я наполнил статьи разным количеством лоремов, а ссылки разной длиной и набором заголовков. Имея контент для работы, мы можем приступить к верстке.
Прокрутка макетов
В этом компоненте есть 3 различных типа областей прокрутки:
- Навигация (розовая) с возможностью горизонтальной прокрутки.
- Область содержимого (синяя) прокручивается по горизонтали.
- Каждый элемент статьи (зеленый) прокручивается по вертикали.
Существует два разных типа элементов, участвующих в прокрутке:
- Окно
Поле с определенными размерами, имеющее стиль свойстваoverflow
. - Негабаритная поверхность
В этом макете это контейнеры списков: навигационные ссылки, статьи разделов и содержимое статей.
Макет <snap-tabs>
Макет верхнего уровня, который я выбрал, был гибким (Flexbox). Я установил направление column
, чтобы заголовок и раздел располагались вертикально. Это наше первое окно прокрутки, и оно скрывает все, а переполнение скрыто. В заголовке и разделе скоро будет использоваться прокрутка, как в отдельных зонах.
<snap-tabs> <header></header> <section></section> </snap-tabs>
snap-tabs { display: flex; flex-direction: column; /* establish primary containing box */ overflow: hidden; position: relative; & > section { /* be pushy about consuming all space */ block-size: 100%; } & > header { /* defend againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
Возвращаясь к красочной диаграмме с тремя прокрутками:
-
<header>
теперь готов стать (розовым) контейнером прокрутки. -
<section>
подготовлен как (синий) контейнер прокрутки.
Кадры, которые я выделил ниже с помощью VisBug, помогают нам увидеть окна, созданные контейнерами прокрутки.
Макет вкладок <header>
Следующий макет почти такой же: я использую flex для создания вертикального порядка.
<snap-tabs> <header> <nav></nav> <span class="snap-indicator"></span> </header> <section></section> </snap-tabs>
header { display: flex; flex-direction: column; }
.snap-indicator
должен перемещаться горизонтально вместе с группой ссылок, и этот макет заголовка помогает подготовить этот этап. Здесь нет абсолютно позиционированных элементов!
Далее стили прокрутки. Оказывается, мы можем использовать стили прокрутки для двух наших горизонтальных областей прокрутки (заголовок и раздел), поэтому я создал служебный класс .scroll-snap-x
.
.scroll-snap-x {
/* browser decide if x is ok to scroll and show bars on, y hidden */
overflow: auto hidden;
/* prevent scroll chaining on x scroll */
overscroll-behavior-x: contain;
/* scrolling should snap children on x */
scroll-snap-type: x mandatory;
@media (hover: none) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
Каждому из них требуется переполнение по оси X, ограничение прокрутки для предотвращения чрезмерной прокрутки, скрытые полосы прокрутки для сенсорных устройств и, наконец, привязка прокрутки для блокировки областей представления контента. Наш порядок вкладок на клавиатуре доступен, и любые взаимодействия естественным образом направляют фокус. Контейнеры с привязкой к прокрутке также имеют удобное взаимодействие с клавиатурой в стиле карусели.
Макет заголовка вкладок <nav>
Навигационные ссылки должны располагаться в линию без разрывов строк, по центру по вертикали, и каждый элемент ссылки должен быть привязан к контейнеру привязки к прокрутке. Swift работает для CSS 2021 года!
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
Каждая ссылка имеет собственный стиль и размер, поэтому в макете навигации необходимо указать только направление и поток. Уникальная ширина элементов навигации делает переход между вкладками увлекательным, поскольку индикатор подстраивает свою ширину под новую цель. В зависимости от того, сколько здесь элементов, браузер будет отображать полосу прокрутки или нет.
Макет вкладок <section>
Этот раздел является гибким элементом и должен быть доминирующим потребителем пространства. Также необходимо создать столбцы для размещения статей. И снова быстрая работа над CSS 2021! block-size: 100%
растягивает этот элемент, чтобы максимально заполнить родительский элемент, затем для собственного макета он создает серию столбцов, ширина которых составляет 100%
ширины родительского элемента. Проценты здесь отлично работают, потому что мы наложили строгие ограничения на родительский элемент.
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
Это как если бы мы говорили: «Расширяйтесь по вертикали настолько, насколько это возможно, настойчиво» (помните заголовок, который мы установили для flex-shrink: 0
: это защита от этого расширения), который устанавливает высоту строки для набор колонн во всю высоту. Стиль auto-flow
предписывает сетке всегда располагать дочерние элементы в горизонтальной линии, без переноса, именно так, как мы хотим; для переполнения родительского окна.
Мне иногда трудно уложить в голове эти мысли! Этот элемент раздела вписывается в коробку, но также создает набор коробок. Надеюсь, иллюстрации и пояснения помогут.
Макет вкладок <article>
Пользователь должен иметь возможность прокручивать содержимое статьи, а полосы прокрутки должны отображаться только в случае переполнения. Эти элементы статьи находятся в аккуратном положении. Они одновременно являются родительским элементом прокрутки и дочерним элементом прокрутки. Здесь браузер действительно справляется с некоторыми сложными взаимодействиями с сенсорным экраном, мышью и клавиатурой.
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
Я решил, чтобы статьи прикреплялись к родительскому скроллеру. Мне очень нравится, как элементы навигационных ссылок и элементы статьи привязываются к началу строки соответствующих контейнеров прокрутки. Это выглядит и ощущается как гармоничные отношения.
Статья является дочерним элементом сетки, и ее размер заранее определен как область просмотра, которую мы хотим предоставить для прокрутки. Это означает, что мне не нужны стили высоты или ширины, мне просто нужно определить, как происходит переполнение. Я установил для overflow-y значение auto, а затем также перехватываю взаимодействие прокрутки с помощью удобного свойства overscroll-behavior.
Обзор трех областей прокрутки
Ниже я выбрал в настройках своей системы «всегда показывать полосы прокрутки». Я думаю, что для макета вдвойне важно работать с включенным этим параметром, поскольку мне приходится проверять макет и оркестровку прокрутки.
Я думаю, что наличие поля полосы прокрутки в этом компоненте помогает четко показать, где находятся области прокрутки, какое направление они поддерживают и как они взаимодействуют друг с другом. Рассмотрим, что каждый из этих фреймов окон прокрутки также является гибким или сеточным родительским элементом макета.
DevTools может помочь нам визуализировать это:
Макеты прокрутки готовы: привязка, глубокая связь и доступ с клавиатуры. Прочная основа для улучшения UX, стиля и удовольствия.
Основные характеристики
Дочерние элементы, привязанные к прокрутке, сохраняют свое заблокированное положение во время изменения размера. Это означает, что JavaScript не нужно будет отображать что-либо при повороте устройства или изменении размера браузера. Попробуйте это в режиме устройства Chromium DevTools, выбрав любой режим, кроме Responsive , а затем изменив размер рамки устройства. Обратите внимание, что элемент остается в поле зрения и заблокирован своим содержимым. Это стало доступно с тех пор, как Chromium обновил свою реализацию, чтобы она соответствовала спецификации. Вот сообщение в блоге об этом.
Анимация
Цель работы по анимации — четко связать взаимодействие с обратной связью пользовательского интерфейса. Это помогает пользователю (надеюсь) беспрепятственно открыть весь контент. Я буду добавлять движение целенаправленно и условно. Теперь пользователи могут указывать свои предпочтения в движении в своей операционной системе, и мне очень нравится реагировать на их предпочтения в своих интерфейсах.
Я свяжу подчеркивание табуляции с позицией прокрутки статьи. Привязка не только обеспечивает хорошее выравнивание, но и закрепляет начало и конец анимации. Это сохраняет <nav>
, который действует как мини-карта , связанным с содержимым. Мы будем проверять предпочтения пользователя в отношении движения как с помощью CSS, так и с помощью JS. Есть несколько замечательных мест, на которые стоит обратить внимание!
Поведение прокрутки
Существует возможность улучшить поведение при движении как :target
, так и element.scrollIntoView()
. По умолчанию это мгновенно. Браузер просто устанавливает положение прокрутки. А что, если мы захотим перейти в эту позицию прокрутки, а не моргать там?
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
Поскольку здесь мы представляем движение, а также движение, которое пользователь не контролирует (например, прокрутку), мы применяем этот стиль только в том случае, если у пользователя нет предпочтений в своей операционной системе в отношении уменьшения движения. Таким образом, мы вводим движение прокрутки только для тех, кого это устраивает.
Индикатор вкладок
Цель этой анимации — помочь связать индикатор с состоянием контента. Я решил раскрасить стили кроссфейдной border-bottom
для пользователей, которые предпочитают ограниченное движение, и анимацию скольжения и затухания со ссылкой на прокрутку для пользователей, которых устраивает движение.
В Chromium Devtools я могу переключить предпочтения и продемонстрировать два разных стиля перехода. Я получил массу удовольствия, создавая это.
@media (prefers-reduced-motion: reduce) {
snap-tabs > header a {
border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
transition: color .7s ease, border-color .5s ease;
&:is(:target,:active,[active]) {
color: var(--text-active-color);
border-block-end-color: hsl(var(--accent));
}
}
snap-tabs .snap-indicator {
visibility: hidden;
}
}
Я скрываю .snap-indicator
, когда пользователь предпочитает ограниченное движение, поскольку он мне больше не нужен. Затем я заменяю его стилями border-block-end
и transition
. Также обратите внимание при взаимодействии вкладок, что активный элемент навигации не только имеет подчеркивание бренда, но и цвет текста становится темнее. Активный элемент имеет более высокий цветовой контраст текста и яркий акцент подсветки.
Всего несколько дополнительных строк CSS заставят кого-то почувствовать себя увиденным (в том смысле, что мы внимательно относимся к его предпочтениям в движении). Я люблю это.
@scroll-timeline
В предыдущем разделе я показал вам, как я обрабатываю стили кроссфейда с уменьшенным движением, а в этом разделе я покажу вам, как я связал индикатор и область прокрутки вместе. Далее вас ждут забавные экспериментальные вещи. Надеюсь, вы так же взволнованы, как и я.
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
Сначала я проверяю предпочтения пользователя в отношении движения с помощью JavaScript. Если результатом этого является false
, что означает, что пользователь предпочитает ограниченное движение, мы не будем запускать какие-либо эффекты движения, связывающие прокрутку.
if (motionOK) {
// motion based animation code
}
На момент написания этой статьи поддержка @scroll-timeline
браузером отсутствует. Это черновая спецификация, содержащая только экспериментальные реализации. Однако у него есть полифилл, который я использую в этой демонстрации.
ScrollTimeline
Хотя CSS и JavaScript могут создавать временные шкалы прокрутки, я выбрал JavaScript, чтобы иметь возможность использовать измерения элементов в анимации в реальном времени.
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection, // snap-tabs > section
orientation: 'inline', // scroll in the direction letters flow
fill: 'both', // bi-directional linking
});
Я хочу, чтобы одна вещь следовала за позицией прокрутки другой, и, создавая ScrollTimeline
я определяю драйвер ссылки прокрутки, scrollSource
. Обычно анимация в Интернете выполняется в соответствии с глобальным интервалом времени, но с помощью специальной sectionScrollTimeline
в памяти я могу все это изменить.
tabindicator.animate({
transform: ...,
width: ...,
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Прежде чем я перейду к ключевым кадрам анимации, я думаю, важно отметить, что последователь прокрутки, tabindicator
, будет анимироваться на основе пользовательской временной шкалы — прокрутки нашего раздела. На этом связь завершена, но отсутствует последний ингредиент — точки с сохранением состояния для анимации между ними, также известные как ключевые кадры.
Динамические ключевые кадры
Существует действительно мощный чисто декларативный CSS-способ анимации с помощью @scroll-timeline
, но анимация, которую я выбрал, была слишком динамичной. Невозможно переключаться между auto
шириной и нет способа динамически создавать несколько ключевых кадров в зависимости от длины дочерних элементов.
Однако JavaScript знает, как получить эту информацию, поэтому мы сами будем перебирать дочерние элементы и получать вычисленные значения во время выполнения:
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Для каждого tabnavitem
деструктурируйте позицию offsetLeft
и верните строку, которая использует ее в качестве значения translateX
. Это создаст 4 ключевых кадра преобразования для анимации. То же самое делается и с шириной: каждому задается вопрос, какова его динамическая ширина, а затем она используется в качестве значения ключевого кадра.
Вот пример вывода, основанный на моих шрифтах и настройках браузера:
Ключевые кадры TranslateX:
[...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`)
// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]
Ширина ключевых кадров:
[...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]
Подводя итог стратегии, индикатор табуляции теперь будет анимироваться по 4 ключевым кадрам в зависимости от положения привязки прокрутки прокрутки раздела. Точки привязки создают четкое разграничение между ключевыми кадрами и действительно добавляют ощущения синхронизации анимации.
Пользователь управляет анимацией своим взаимодействием, видя, как ширина и положение индикатора меняются от одного раздела к другому, идеально отслеживая прокрутку.
Возможно, вы не заметили, но я очень горжусь переходом цвета при выборе выделенного элемента навигации.
Невыбранный светло-серый цвет кажется еще более отодвинутым назад, когда выделенный элемент становится более контрастным. Обычно изменение цвета текста происходит, например, при наведении курсора мыши и при выделении, но на следующем уровне происходит изменение цвета при прокрутке, синхронизированное с индикатором подчеркивания.
Вот как я это сделал:
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
});
Для каждой ссылки навигации по вкладкам требуется новая цветовая анимация, отслеживающая ту же временную шкалу прокрутки, что и индикатор подчеркивания. Я использую ту же временную шкалу, что и раньше: поскольку ее роль заключается в создании тика при прокрутке, мы можем использовать этот тик в любом типе анимации, который нам нужен. Как и раньше, я создаю в цикле 4 ключевых кадра и возвращаю цвета.
[...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
// results in 4 array items, which represent 4 keyframe states
// [
"var(--text-active-color)",
"var(--text-color)",
"var(--text-color)",
"var(--text-color)",
]
Ключевой кадр с цветом var(--text-active-color)
выделяет ссылку, в остальном это стандартный цвет текста. Вложенный цикл делает это относительно простым, поскольку внешний цикл — это каждый элемент навигации, а внутренний цикл — персональные ключевые кадры каждого элемента навигации. Я проверяю, совпадает ли элемент внешнего цикла с элементом внутреннего цикла, и использую это, чтобы узнать, когда он выбран.
Мне было очень весело писать это. Так много.
Еще больше улучшений JavaScript
Стоит напомнить, что суть того, что я вам здесь показываю, работает без JavaScript. С учетом сказанного, давайте посмотрим, как мы можем улучшить его, когда будет доступен JS.
Глубокие ссылки
Глубокие ссылки — это скорее мобильный термин, но я думаю, что цель глубокой ссылки здесь достигается с помощью вкладок, поскольку вы можете поделиться URL-адресом непосредственно с содержимым вкладки. Браузер на странице перейдет к идентификатору, который соответствует хешу URL-адреса. Я обнаружил, что этот обработчик onload
оказывает влияние на разные платформы.
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
Синхронизация завершения прокрутки
Наши пользователи не всегда щелкают мышью или используют клавиатуру, иногда они просто свободно прокручивают страницу, как и должно быть. Когда скроллер раздела перестанет прокручиваться, то, где бы он ни находился, должно быть сопоставлено с верхней панелью навигации.
Вот как я жду окончания прокрутки: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });
Всякий раз, когда разделы прокручиваются, очистите тайм-аут раздела, если он есть, и начните новый. Когда разделы перестают прокручиваться, не очищайте тайм-аут и запускайте 100 мс после отдыха. Когда он сработает, вызовите функцию, которая пытается выяснить, где остановился пользователь.
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth;
const matchingNavItem = tabnavitems[i];
matchingNavItem && setActiveTab(matchingNavItem);
};
Предполагая, что прокрутка зафиксирована, деление текущей позиции прокрутки на ширину области прокрутки должно дать целое число, а не десятичное число. Затем я пытаюсь получить navitem из нашего кэша через этот рассчитанный индекс, и если он что-то находит, я отправляю совпадение, чтобы оно стало активным.
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active');
tabbtn.setAttribute('active', '');
tabbtn.scrollIntoView();
};
Установка активной вкладки начинается с очистки любой активной в данный момент вкладки, а затем присвоения входящему элементу навигации атрибута активного состояния. Вызов scrollIntoView()
имеет забавное взаимодействие с CSS, на которое стоит обратить внимание.
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
В CSS-утилиту привязки горизонтальной прокрутки мы вложили медиа-запрос, который применяет smooth
прокрутку, если пользователь терпим к движению. JavaScript может свободно выполнять вызовы для прокрутки элементов в поле зрения, а CSS может декларативно управлять UX. Иногда они составляют очаровательную пару.
Заключение
Теперь, когда вы знаете, как я это сделал, как бы вы поступили?! Это создает забавную компонентную архитектуру! Кто собирается сделать 1-ю версию со слотами на любимом фреймворке? 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте глюк , напишите мне в Твиттере свою версию, и я добавлю ее в раздел ремиксов сообщества ниже.
Ремиксы сообщества
- @devnook , @rob_dodson и @DasSurma с веб-компонентами: статья .
- @jhvanderschee с кнопками: Codepen .
Базовый обзор того, как создать компонент вкладок, аналогичный тем, которые есть в приложениях для iOS и Android.
В этом посте я хочу поделиться мыслями о создании веб-компонента «Вкладки», который будет адаптивным, поддерживает ввод с нескольких устройств и работает во всех браузерах. Попробуйте демо .
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Вкладки являются распространенным компонентом дизайн-систем, но могут принимать самые разные формы. Сначала были вкладки рабочего стола, построенные на элементе <frame>
, а теперь у нас есть удобные мобильные компоненты, которые анимируют контент на основе физических свойств. Все они пытаются сделать одно и то же: сэкономить место.
Сегодня основой пользовательского опыта работы с вкладками является область навигации по кнопкам, которая переключает видимость контента в рамке дисплея. Множество различных областей контента занимают одно и то же пространство, но представлены условно в зависимости от кнопки, выбранной в навигации.
Веб-тактика
В целом этот компонент показался мне довольно простым в создании благодаря нескольким важным функциям веб-платформы:
-
scroll-snap-points
для элегантного взаимодействия с помощью смахивания и клавиатуры с соответствующими позициями остановки прокрутки - Глубокие ссылки через хэши URL-адресов для поддержки привязки прокрутки на странице и поддержки совместного использования браузером.
- Поддержка чтения с экрана с разметкой элементов
<a>
иid="#hash"
-
prefers-reduced-motion
для включения плавных переходов и мгновенной прокрутки страницы. - Встроенная веб-функция
@scroll-timeline
для динамического подчеркивания и изменения цвета выбранной вкладки.
HTML
По сути, UX здесь таков: щелкните ссылку, URL-адрес будет представлять состояние вложенной страницы, а затем вы увидите обновление области контента по мере прокрутки браузера к соответствующему элементу.
Здесь есть некоторые элементы структурного контента: ссылки и :target
. Нам нужен список ссылок, для которых отлично подходит <nav>
, и список элементов <article>
, для которых отлично подходит <section>
. Каждый хеш ссылки будет соответствовать разделу, позволяя браузеру прокручивать элементы посредством привязки.
Например, при нажатии на ссылку автоматически фокусируется статья :target
в Chrome 89, JS не требуется. Затем пользователь может, как всегда, прокручивать содержимое статьи с помощью своего устройства ввода. Как указано в разметке, это бесплатный контент.
Для организации вкладок я использовал следующую разметку:
<snap-tabs>
<header>
<nav>
<a></a>
<a></a>
<a></a>
<a></a>
</nav>
</header>
<section>
<article></article>
<article></article>
<article></article>
<article></article>
</section>
</snap-tabs>
Я могу установить связи между элементами <a>
и <article>
с помощью свойств href
и id
следующим образом:
<snap-tabs>
<header>
<nav>
<a href="#responsive"></a>
<a href="#accessible"></a>
<a href="#overscroll"></a>
<a href="#more"></a>
</nav>
</header>
<section>
<article id="responsive"></article>
<article id="accessible"></article>
<article id="overscroll"></article>
<article id="more"></article>
</section>
</snap-tabs>
Затем я наполнил статьи разным количеством лоремов, а ссылки разной длиной и набором заголовков. Имея контент для работы, мы можем приступить к верстке.
Прокрутка макетов
В этом компоненте есть 3 различных типа областей прокрутки:
- Навигация (розовая) с возможностью горизонтальной прокрутки.
- Область содержимого (синяя) прокручивается по горизонтали.
- Каждый элемент статьи (зеленый) прокручивается по вертикали.
Существует два разных типа элементов, участвующих в прокрутке:
- Окно
Поле с определенными размерами, имеющее стиль свойстваoverflow
. - Негабаритная поверхность
В этом макете это контейнеры списков: навигационные ссылки, статьи разделов и содержимое статей.
Макет <snap-tabs>
Макет верхнего уровня, который я выбрал, был гибким (Flexbox). Я установил направление column
, чтобы заголовок и раздел располагались вертикально. Это наше первое окно прокрутки, и оно скрывает все, а переполнение скрыто. В заголовке и разделе скоро будет использоваться прокрутка, как в отдельных зонах.
<snap-tabs> <header></header> <section></section> </snap-tabs>
snap-tabs { display: flex; flex-direction: column; /* establish primary containing box */ overflow: hidden; position: relative; & > section { /* be pushy about consuming all space */ block-size: 100%; } & > header { /* defend againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
Возвращаясь к красочной диаграмме с тремя прокрутками:
-
<header>
теперь готов стать (розовым) контейнером прокрутки. -
<section>
подготовлен как (синий) контейнер прокрутки.
Кадры, которые я выделил ниже с помощью VisBug, помогают нам увидеть окна, созданные контейнерами прокрутки.
Макет вкладок <header>
Следующий макет почти такой же: я использую flex для создания вертикального порядка.
<snap-tabs> <header> <nav></nav> <span class="snap-indicator"></span> </header> <section></section> </snap-tabs>
header { display: flex; flex-direction: column; }
.snap-indicator
должен перемещаться горизонтально вместе с группой ссылок, и этот макет заголовка помогает подготовить этот этап. Здесь нет абсолютно позиционированных элементов!
Далее стили прокрутки. Оказывается, мы можем использовать стили прокрутки для двух наших горизонтальных областей прокрутки (заголовок и раздел), поэтому я создал служебный класс .scroll-snap-x
.
.scroll-snap-x {
/* browser decide if x is ok to scroll and show bars on, y hidden */
overflow: auto hidden;
/* prevent scroll chaining on x scroll */
overscroll-behavior-x: contain;
/* scrolling should snap children on x */
scroll-snap-type: x mandatory;
@media (hover: none) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
Каждому из них требуется переполнение по оси X, ограничение прокрутки для предотвращения чрезмерной прокрутки, скрытые полосы прокрутки для сенсорных устройств и, наконец, привязка прокрутки для блокировки областей представления контента. Наш порядок вкладок на клавиатуре доступен, и любые взаимодействия естественным образом направляют фокус. Контейнеры с привязкой к прокрутке также имеют удобное взаимодействие с клавиатурой в стиле карусели.
Макет заголовка вкладок <nav>
Навигационные ссылки должны располагаться в линию без разрывов строк, по центру по вертикали, и каждый элемент ссылки должен быть привязан к контейнеру привязки к прокрутке. Swift работает для CSS 2021 года!
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
Каждая ссылка имеет собственный стиль и размер, поэтому в макете навигации необходимо указать только направление и поток. Уникальная ширина элементов навигации делает переход между вкладками увлекательным, поскольку индикатор подстраивает свою ширину под новую цель. В зависимости от того, сколько здесь элементов, браузер будет отображать полосу прокрутки или нет.
Макет вкладок <section>
Этот раздел является гибким элементом и должен быть доминирующим потребителем пространства. Также необходимо создать столбцы для размещения статей. И снова быстрая работа над CSS 2021! block-size: 100%
растягивает этот элемент, чтобы максимально заполнить родительский элемент, затем для собственного макета он создает серию столбцов, ширина которых составляет 100%
ширины родительского элемента. Проценты здесь отлично работают, потому что мы наложили строгие ограничения на родительский элемент.
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
Это как если бы мы говорили: «Расширяйтесь по вертикали настолько, насколько это возможно, настойчиво» (помните заголовок, который мы установили для flex-shrink: 0
: это защита от этого расширения), который устанавливает высоту строки для набор колонн во всю высоту. Стиль auto-flow
предписывает сетке всегда располагать дочерние элементы в горизонтальной линии, без переноса, именно так, как мы хотим; для переполнения родительского окна.
Мне иногда трудно уложить в голове эти мысли! Этот элемент раздела вписывается в коробку, но также создает набор коробок. Надеюсь, иллюстрации и пояснения помогут.
Макет вкладок <article>
Пользователь должен иметь возможность прокручивать содержимое статьи, а полосы прокрутки должны отображаться только в случае переполнения. Эти элементы статьи находятся в аккуратном положении. Они одновременно являются родительским элементом прокрутки и дочерним элементом прокрутки. Здесь браузер действительно справляется с некоторыми сложными взаимодействиями с сенсорным экраном, мышью и клавиатурой.
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
Я решил, чтобы статьи прикреплялись к родительскому скроллеру. Мне очень нравится, как элементы навигационных ссылок и элементы статьи привязываются к началу строки соответствующих контейнеров прокрутки. Это выглядит и ощущается как гармоничные отношения.
Статья является дочерним элементом сетки, и ее размер заранее определен как область просмотра, которую мы хотим предоставить для прокрутки. Это означает, что мне не нужны стили высоты или ширины, мне просто нужно определить, как происходит переполнение. Я установил для overflow-y значение auto, а затем также перехватываю взаимодействие прокрутки с помощью удобного свойства overscroll-behavior.
Обзор трех областей прокрутки
Ниже я выбрал в настройках своей системы «всегда показывать полосы прокрутки». Я думаю, что для макета вдвойне важно работать с включенным этим параметром, поскольку мне приходится проверять макет и оркестровку прокрутки.
Я думаю, что наличие поля полосы прокрутки в этом компоненте помогает четко показать, где находятся области прокрутки, какое направление они поддерживают и как они взаимодействуют друг с другом. Рассмотрим, что каждый из этих фреймов окон прокрутки также является гибким или сеточным родительским элементом макета.
DevTools может помочь нам визуализировать это:
Макеты прокрутки готовы: привязка, глубокая связь и доступ с клавиатуры. Прочная основа для улучшения UX, стиля и удовольствия.
Основные характеристики
Дочерние элементы, привязанные к прокрутке, сохраняют свое заблокированное положение во время изменения размера. Это означает, что JavaScript не нужно будет отображать что-либо при повороте устройства или изменении размера браузера. Попробуйте это в режиме устройства Chromium DevTools, выбрав любой режим, кроме Responsive , а затем изменив размер рамки устройства. Обратите внимание, что элемент остается в поле зрения и заблокирован своим содержимым. Это стало доступно с тех пор, как Chromium обновил свою реализацию, чтобы она соответствовала спецификации. Вот сообщение в блоге об этом.
Анимация
Цель работы по анимации — четко связать взаимодействие с обратной связью пользовательского интерфейса. Это помогает пользователю (надеюсь) беспрепятственно открыть весь контент. Я буду добавлять движение целенаправленно и условно. Теперь пользователи могут указывать свои предпочтения в движении в своей операционной системе, и мне очень нравится реагировать на их предпочтения в своих интерфейсах.
Я свяжу подчеркивание табуляции с позицией прокрутки статьи. Привязка не только обеспечивает хорошее выравнивание, но и закрепляет начало и конец анимации. Это сохраняет <nav>
, который действует как мини-карта , связанным с содержимым. Мы будем проверять предпочтения пользователя в отношении движения как с помощью CSS, так и с помощью JS. Есть несколько замечательных мест, на которые стоит обратить внимание!
Поведение прокрутки
Существует возможность улучшить поведение при движении как :target
, так и element.scrollIntoView()
. По умолчанию это мгновенно. Браузер просто устанавливает положение прокрутки. А что, если мы захотим перейти в эту позицию прокрутки, а не моргать там?
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
Поскольку здесь мы представляем движение, а также движение, которое пользователь не контролирует (например, прокрутку), мы применяем этот стиль только в том случае, если у пользователя нет предпочтений в своей операционной системе в отношении уменьшения движения. Таким образом, мы вводим движение прокрутки только для тех, кого это устраивает.
Индикатор вкладок
Цель этой анимации — помочь связать индикатор с состоянием контента. Я решил раскрасить стили кроссфейдной border-bottom
для пользователей, которые предпочитают ограниченное движение, и анимацию скольжения и затухания со ссылкой на прокрутку для пользователей, которых устраивает движение.
В Chromium Devtools я могу переключить предпочтения и продемонстрировать два разных стиля перехода. Я получил массу удовольствия, создавая это.
@media (prefers-reduced-motion: reduce) {
snap-tabs > header a {
border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
transition: color .7s ease, border-color .5s ease;
&:is(:target,:active,[active]) {
color: var(--text-active-color);
border-block-end-color: hsl(var(--accent));
}
}
snap-tabs .snap-indicator {
visibility: hidden;
}
}
Я скрываю .snap-indicator
, когда пользователь предпочитает ограниченное движение, поскольку он мне больше не нужен. Затем я заменяю его стилями border-block-end
и transition
. Также обратите внимание при взаимодействии вкладок, что активный элемент навигации не только имеет подчеркивание бренда, но и цвет текста становится темнее. Активный элемент имеет более высокий цветовой контраст текста и яркий акцент подсветки.
Всего несколько дополнительных строк CSS заставят кого-то почувствовать себя увиденным (в том смысле, что мы внимательно относимся к его предпочтениям в движении). Я люблю это.
@scroll-timeline
В предыдущем разделе я показал вам, как я обрабатываю стили кроссфейда с уменьшенным движением, а в этом разделе я покажу вам, как я связал индикатор и область прокрутки вместе. Далее вас ждут забавные экспериментальные вещи. Надеюсь, вы так же взволнованы, как и я.
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
Сначала я проверяю предпочтения пользователя в отношении движения с помощью JavaScript. Если результатом этого является false
, что означает, что пользователь предпочитает ограниченное движение, мы не будем запускать какие-либо эффекты движения, связывающие прокрутку.
if (motionOK) {
// motion based animation code
}
На момент написания этой статьи поддержка @scroll-timeline
браузером отсутствует. Это черновая спецификация, содержащая только экспериментальные реализации. Однако у него есть полифилл, который я использую в этой демонстрации.
ScrollTimeline
Хотя CSS и JavaScript могут создавать временные шкалы прокрутки, я выбрал JavaScript, чтобы иметь возможность использовать измерения элементов в анимации в реальном времени.
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection, // snap-tabs > section
orientation: 'inline', // scroll in the direction letters flow
fill: 'both', // bi-directional linking
});
Я хочу, чтобы одна вещь следовала за позицией прокрутки другой, и, создавая ScrollTimeline
я определяю драйвер ссылки прокрутки, scrollSource
. Обычно анимация в Интернете выполняется в соответствии с глобальным интервалом времени, но с помощью специальной sectionScrollTimeline
в памяти я могу все это изменить.
tabindicator.animate({
transform: ...,
width: ...,
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Прежде чем я перейду к ключевым кадрам анимации, я думаю, важно отметить, что последователь прокрутки, tabindicator
, будет анимироваться на основе пользовательской временной шкалы — прокрутки нашего раздела. На этом связь завершена, но отсутствует последний ингредиент — точки с сохранением состояния для анимации между ними, также известные как ключевые кадры.
Динамические ключевые кадры
Существует действительно мощный чисто декларативный CSS-способ анимации с помощью @scroll-timeline
, но анимация, которую я выбрал, была слишком динамичной. Невозможно переключаться между auto
шириной и нет способа динамически создавать несколько ключевых кадров в зависимости от длины дочерних элементов.
Однако JavaScript знает, как получить эту информацию, поэтому мы сами будем перебирать дочерние элементы и получать вычисленные значения во время выполнения:
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Для каждого tabnavitem
деструктурируйте позицию offsetLeft
и верните строку, которая использует ее в качестве значения translateX
. Это создаст 4 ключевых кадра преобразования для анимации. То же самое делается и с шириной: каждому задается вопрос, какова его динамическая ширина, а затем она используется в качестве значения ключевого кадра.
Вот пример вывода, основанный на моих шрифтах и настройках браузера:
Ключевые кадры TranslateX:
[...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`)
// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]
Ширина ключевых кадров:
[...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]
Подводя итог стратегии, индикатор табуляции теперь будет анимироваться по 4 ключевым кадрам в зависимости от положения привязки прокрутки прокрутки раздела. Точки привязки создают четкое разграничение между ключевыми кадрами и действительно добавляют ощущения синхронизации анимации.
Пользователь управляет анимацией своим взаимодействием, видя, как ширина и положение индикатора меняются от одного раздела к другому, идеально отслеживая прокрутку.
Возможно, вы не заметили, но я очень горжусь переходом цвета при выборе выделенного элемента навигации.
Невыбранный светло-серый цвет кажется еще более отодвинутым назад, когда выделенный элемент становится более контрастным. Обычно изменение цвета текста происходит, например, при наведении курсора мыши и при выделении, но на следующем уровне происходит изменение цвета при прокрутке, синхронизированное с индикатором подчеркивания.
Вот как я это сделал:
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
});
Для каждой ссылки навигации по вкладкам требуется новая цветовая анимация, отслеживающая ту же временную шкалу прокрутки, что и индикатор подчеркивания. Я использую ту же временную шкалу, что и раньше: поскольку ее роль заключается в создании тика при прокрутке, мы можем использовать этот тик в любом типе анимации, который нам нужен. Как и раньше, я создаю в цикле 4 ключевых кадра и возвращаю цвета.
[...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
// results in 4 array items, which represent 4 keyframe states
// [
"var(--text-active-color)",
"var(--text-color)",
"var(--text-color)",
"var(--text-color)",
]
Ключевой кадр с цветом var(--text-active-color)
выделяет ссылку, в остальном это стандартный цвет текста. Вложенный цикл делает это относительно простым, поскольку внешний цикл — это каждый элемент навигации, а внутренний цикл — персональные ключевые кадры каждого элемента навигации. Я проверяю, совпадает ли элемент внешнего цикла с элементом внутреннего цикла, и использую это, чтобы узнать, когда он выбран.
Мне было очень весело писать это. Так много.
Еще больше улучшений JavaScript
Стоит напомнить, что суть того, что я вам здесь показываю, работает без JavaScript. С учетом сказанного, давайте посмотрим, как мы можем улучшить его, когда будет доступен JS.
Глубокие ссылки
Глубокие ссылки — это скорее мобильный термин, но я думаю, что цель глубокой ссылки здесь достигается с помощью вкладок, поскольку вы можете поделиться URL-адресом непосредственно с содержимым вкладки. Браузер на странице перейдет к идентификатору, который соответствует хешу URL-адреса. Я обнаружил, что этот обработчик onload
оказывает влияние на разные платформы.
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
Синхронизация завершения прокрутки
Наши пользователи не всегда щелкают мышью или используют клавиатуру, иногда они просто свободно прокручивают страницу, как и должно быть. Когда скроллер раздела перестанет прокручиваться, то, где бы он ни находился, должно быть сопоставлено с верхней панелью навигации.
Вот как я жду окончания прокрутки: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });
Всякий раз, когда разделы прокручиваются, очистите тайм-аут раздела, если он есть, и начните новый. Когда разделы перестают прокручиваться, не очищайте тайм-аут и запускайте 100 мс после отдыха. Когда он сработает, вызовите функцию, которая пытается выяснить, где остановился пользователь.
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth;
const matchingNavItem = tabnavitems[i];
matchingNavItem && setActiveTab(matchingNavItem);
};
Предполагая, что прокрутка зафиксирована, деление текущей позиции прокрутки на ширину области прокрутки должно дать целое число, а не десятичное число. Затем я пытаюсь получить navitem из нашего кэша через этот рассчитанный индекс, и если он что-то находит, я отправляю совпадение, чтобы оно стало активным.
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active');
tabbtn.setAttribute('active', '');
tabbtn.scrollIntoView();
};
Установка активной вкладки начинается с очистки любой активной в данный момент вкладки, а затем присвоения входящему элементу навигации атрибута активного состояния. Вызов scrollIntoView()
имеет забавное взаимодействие с CSS, на которое стоит обратить внимание.
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
В CSS-утилиту привязки горизонтальной прокрутки мы вложили медиа-запрос, который применяет smooth
прокрутку, если пользователь терпим к движению. JavaScript может свободно выполнять вызовы для прокрутки элементов в поле зрения, а CSS может декларативно управлять UX. Иногда они составляют очаровательную пару.
Заключение
Теперь, когда вы знаете, как я это сделал, как бы вы поступили?! Это создает забавную компонентную архитектуру! Кто собирается сделать 1-ю версию со слотами на любимом фреймворке? 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте глюк , напишите мне в Твиттере свою версию, и я добавлю ее в раздел ремиксов сообщества ниже.
Ремиксы сообщества
- @devnook , @rob_dodson и @DasSurma с веб-компонентами: статья .
- @jhvanderschee с кнопками: Codepen .
Базовый обзор того, как создать компонент вкладок, аналогичный тем, которые есть в приложениях для iOS и Android.
В этом посте я хочу поделиться мыслями о создании веб-компонента «Вкладки», который будет адаптивным, поддерживает ввод с нескольких устройств и работает во всех браузерах. Попробуйте демо .
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Вкладки являются распространенным компонентом дизайн-систем, но могут принимать самые разные формы. Сначала были вкладки рабочего стола, построенные на элементе <frame>
, а теперь у нас есть удобные мобильные компоненты, которые анимируют контент на основе физических свойств. Все они пытаются сделать одно и то же: сэкономить место.
Сегодня основой пользовательского опыта работы с вкладками является область навигации по кнопкам, которая переключает видимость контента в рамке дисплея. Множество различных областей контента занимают одно и то же пространство, но представлены условно в зависимости от кнопки, выбранной в навигации.
Веб-тактика
В целом этот компонент показался мне довольно простым в создании благодаря нескольким важным функциям веб-платформы:
-
scroll-snap-points
для элегантного взаимодействия с помощью смахивания и клавиатуры с соответствующими позициями остановки прокрутки - Глубокие ссылки через хэши URL-адресов для поддержки привязки прокрутки на странице и поддержки совместного использования браузером.
- Поддержка чтения с экрана с разметкой элементов
<a>
иid="#hash"
-
prefers-reduced-motion
для включения плавных переходов и мгновенной прокрутки страницы. - Встроенная веб-функция
@scroll-timeline
для динамического подчеркивания и изменения цвета выбранной вкладки.
HTML
По сути, UX здесь таков: щелкните ссылку, URL-адрес будет представлять состояние вложенной страницы, а затем вы увидите обновление области контента по мере прокрутки браузера к соответствующему элементу.
Здесь есть некоторые элементы структурного контента: ссылки и :target
. Нам нужен список ссылок, для которых отлично подходит <nav>
, и список элементов <article>
, для которых отлично подходит <section>
. Каждый хеш ссылки будет соответствовать разделу, позволяя браузеру прокручивать элементы посредством привязки.
Например, при нажатии на ссылку автоматически фокусируется статья :target
в Chrome 89, JS не требуется. Затем пользователь может, как всегда, прокручивать содержимое статьи с помощью своего устройства ввода. Как указано в разметке, это бесплатный контент.
Для организации вкладок я использовал следующую разметку:
<snap-tabs>
<header>
<nav>
<a></a>
<a></a>
<a></a>
<a></a>
</nav>
</header>
<section>
<article></article>
<article></article>
<article></article>
<article></article>
</section>
</snap-tabs>
Я могу установить связи между элементами <a>
и <article>
с помощью свойств href
и id
следующим образом:
<snap-tabs>
<header>
<nav>
<a href="#responsive"></a>
<a href="#accessible"></a>
<a href="#overscroll"></a>
<a href="#more"></a>
</nav>
</header>
<section>
<article id="responsive"></article>
<article id="accessible"></article>
<article id="overscroll"></article>
<article id="more"></article>
</section>
</snap-tabs>
Затем я наполнил статьи разным количеством лоремов, а ссылки разной длиной и набором заголовков. Имея контент для работы, мы можем приступить к верстке.
Прокрутка макетов
В этом компоненте есть 3 различных типа областей прокрутки:
- Навигация (розовая) с возможностью горизонтальной прокрутки.
- Область содержимого (синяя) прокручивается по горизонтали.
- Каждый элемент статьи (зеленый) прокручивается по вертикали.
Существует два разных типа элементов, участвующих в прокрутке:
- Окно
Поле с определенными размерами, имеющее стиль свойстваoverflow
. - Негабаритная поверхность
В этом макете это контейнеры списков: навигационные ссылки, статьи разделов и содержимое статей.
Макет <snap-tabs>
Макет верхнего уровня, который я выбрал, был гибким (Flexbox). Я установил направление column
, чтобы заголовок и раздел располагались вертикально. Это наше первое окно прокрутки, и оно скрывает все, а переполнение скрыто. В заголовке и разделе скоро будет использоваться прокрутка, как в отдельных зонах.
<snap-tabs> <header></header> <section></section> </snap-tabs>
snap-tabs { display: flex; flex-direction: column; /* establish primary containing box */ overflow: hidden; position: relative; & > section { /* be pushy about consuming all space */ block-size: 100%; } & > header { /* defend againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
Возвращаясь к красочной диаграмме с тремя прокрутками:
-
<header>
теперь готов стать (розовым) контейнером прокрутки. -
<section>
подготовлен как (синий) контейнер прокрутки.
Кадры, которые я выделил ниже с помощью VisBug, помогают нам увидеть окна, созданные контейнерами прокрутки.
Макет вкладок <header>
Следующий макет почти такой же: я использую flex для создания вертикального порядка.
<snap-tabs> <header> <nav></nav> <span class="snap-indicator"></span> </header> <section></section> </snap-tabs>
header { display: flex; flex-direction: column; }
.snap-indicator
должен перемещаться горизонтально вместе с группой ссылок, и этот макет заголовка помогает подготовить этот этап. Здесь нет абсолютно позиционированных элементов!
Далее стили прокрутки. Оказывается, мы можем использовать стили прокрутки для двух наших горизонтальных областей прокрутки (заголовок и раздел), поэтому я создал служебный класс .scroll-snap-x
.
.scroll-snap-x {
/* browser decide if x is ok to scroll and show bars on, y hidden */
overflow: auto hidden;
/* prevent scroll chaining on x scroll */
overscroll-behavior-x: contain;
/* scrolling should snap children on x */
scroll-snap-type: x mandatory;
@media (hover: none) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
Каждому из них требуется переполнение по оси X, ограничение прокрутки для предотвращения чрезмерной прокрутки, скрытые полосы прокрутки для сенсорных устройств и, наконец, привязка прокрутки для блокировки областей представления контента. Наш порядок вкладок на клавиатуре доступен, и любые взаимодействия естественным образом направляют фокус. Контейнеры с привязкой к прокрутке также имеют удобное взаимодействие с клавиатурой в стиле карусели.
Макет заголовка вкладок <nav>
Навигационные ссылки должны располагаться в линию без разрывов строк, по центру по вертикали, и каждый элемент ссылки должен быть привязан к контейнеру привязки к прокрутке. Swift работает для CSS 2021 года!
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
Каждая ссылка имеет собственный стиль и размер, поэтому в макете навигации необходимо указать только направление и поток. Уникальная ширина элементов навигации делает переход между вкладками увлекательным, поскольку индикатор подстраивает свою ширину под новую цель. В зависимости от того, сколько здесь элементов, браузер будет отображать полосу прокрутки или нет.
Макет вкладок <section>
Этот раздел является гибким элементом и должен быть доминирующим потребителем пространства. Также необходимо создать столбцы для размещения статей. И снова быстрая работа над CSS 2021! block-size: 100%
растягивает этот элемент, чтобы максимально заполнить родительский элемент, затем для собственного макета он создает серию столбцов, ширина которых составляет 100%
ширины родительского элемента. Проценты здесь отлично работают, потому что мы наложили строгие ограничения на родительский элемент.
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
Это как если бы мы говорили: «Расширяйтесь по вертикали настолько, насколько это возможно, настойчиво» (помните заголовок, который мы установили для flex-shrink: 0
: это защита от этого расширения), который устанавливает высоту строки для набор колонн во всю высоту. Стиль auto-flow
предписывает сетке всегда располагать дочерние элементы в горизонтальной линии, без переноса, именно так, как мы хотим; для переполнения родительского окна.
Мне иногда трудно уложить в голове эти мысли! Этот элемент раздела вписывается в коробку, но также создает набор коробок. Надеюсь, иллюстрации и пояснения помогут.
Макет вкладок <article>
Пользователь должен иметь возможность прокручивать содержимое статьи, а полосы прокрутки должны отображаться только в случае переполнения. Эти элементы статьи находятся в аккуратном положении. Они одновременно являются родительским элементом прокрутки и дочерним элементом прокрутки. Здесь браузер действительно справляется с некоторыми сложными взаимодействиями с сенсорным экраном, мышью и клавиатурой.
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
Я решил, чтобы статьи прикреплялись к родительскому скроллеру. Мне очень нравится, как элементы навигационных ссылок и элементы статьи привязываются к началу строки соответствующих контейнеров прокрутки. Это выглядит и ощущается как гармоничные отношения.
Статья является дочерним элементом сетки, и ее размер заранее определен как область просмотра, которую мы хотим предоставить для прокрутки. Это означает, что мне не нужны стили высоты или ширины, мне просто нужно определить, как происходит переполнение. Я установил для overflow-y значение auto, а затем также перехватываю взаимодействие прокрутки с помощью удобного свойства overscroll-behavior.
Обзор трех областей прокрутки
Ниже я выбрал в настройках своей системы «всегда показывать полосы прокрутки». Я думаю, что для макета вдвойне важно работать с включенным этим параметром, поскольку мне приходится проверять макет и оркестровку прокрутки.
Я думаю, что наличие поля полосы прокрутки в этом компоненте помогает четко показать, где находятся области прокрутки, какое направление они поддерживают и как они взаимодействуют друг с другом. Рассмотрим, что каждый из этих фреймов окон прокрутки также является гибким или сеточным родительским элементом макета.
DevTools может помочь нам визуализировать это:
Макеты прокрутки готовы: привязка, глубокая связь и доступ с клавиатуры. Прочная основа для улучшения UX, стиля и удовольствия.
Основные характеристики
Дочерние элементы, привязанные к прокрутке, сохраняют свое заблокированное положение во время изменения размера. Это означает, что JavaScript не нужно будет отображать что-либо при повороте устройства или изменении размера браузера. Попробуйте это в режиме устройства Chromium DevTools, выбрав любой режим, кроме Responsive , а затем изменив размер рамки устройства. Обратите внимание, что элемент остается в поле зрения и заблокирован своим содержимым. Это стало доступно с тех пор, как Chromium обновил свою реализацию, чтобы она соответствовала спецификации. Вот сообщение в блоге об этом.
Анимация
Цель работы по анимации — четко связать взаимодействие с обратной связью пользовательского интерфейса. Это помогает пользователю (надеюсь) беспрепятственно открыть весь контент. Я буду добавлять движение целенаправленно и условно. Теперь пользователи могут указывать свои предпочтения в движении в своей операционной системе, и мне очень нравится реагировать на их предпочтения в своих интерфейсах.
Я свяжу подчеркивание табуляции с позицией прокрутки статьи. Привязка не только обеспечивает хорошее выравнивание, но и закрепляет начало и конец анимации. Это сохраняет <nav>
, который действует как мини-карта , связанным с содержимым. Мы будем проверять предпочтения пользователя в отношении движения как с помощью CSS, так и с помощью JS. Есть несколько замечательных мест, на которые стоит обратить внимание!
Поведение прокрутки
Существует возможность улучшить поведение при движении как :target
, так и element.scrollIntoView()
. По умолчанию это мгновенно. Браузер просто устанавливает положение прокрутки. А что, если мы захотим перейти в эту позицию прокрутки, а не моргать там?
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
Поскольку здесь мы представляем движение, а также движение, которое пользователь не контролирует (например, прокрутку), мы применяем этот стиль только в том случае, если у пользователя нет предпочтений в своей операционной системе в отношении уменьшения движения. Таким образом, мы вводим движение прокрутки только для тех, кого это устраивает.
Индикатор вкладок
Цель этой анимации — помочь связать индикатор с состоянием контента. Я решил раскрасить стили кроссфейдной border-bottom
для пользователей, которые предпочитают ограниченное движение, и анимацию скольжения и затухания со ссылкой на прокрутку для пользователей, которых устраивает движение.
В Chromium Devtools я могу переключить предпочтения и продемонстрировать два разных стиля перехода. Я получил массу удовольствия, создавая это.
@media (prefers-reduced-motion: reduce) {
snap-tabs > header a {
border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
transition: color .7s ease, border-color .5s ease;
&:is(:target,:active,[active]) {
color: var(--text-active-color);
border-block-end-color: hsl(var(--accent));
}
}
snap-tabs .snap-indicator {
visibility: hidden;
}
}
Я скрываю .snap-indicator
, когда пользователь предпочитает ограниченное движение, поскольку он мне больше не нужен. Затем я заменяю его стилями border-block-end
и transition
. Также обратите внимание при взаимодействии вкладок, что активный элемент навигации не только имеет подчеркивание бренда, но и цвет текста становится темнее. Активный элемент имеет более высокий цветовой контраст текста и яркий акцент подсветки.
Всего несколько дополнительных строк CSS заставят кого-то почувствовать себя увиденным (в том смысле, что мы внимательно относимся к его предпочтениям в движении). Я люблю это.
@scroll-timeline
В предыдущем разделе я показал вам, как я обрабатываю стили кроссфейда с уменьшенным движением, а в этом разделе я покажу вам, как я связал индикатор и область прокрутки вместе. Далее вас ждут забавные экспериментальные вещи. Надеюсь, вы так же взволнованы, как и я.
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
Сначала я проверяю предпочтения пользователя в отношении движения с помощью JavaScript. Если результатом этого является false
, что означает, что пользователь предпочитает ограниченное движение, мы не будем запускать какие-либо эффекты движения, связывающие прокрутку.
if (motionOK) {
// motion based animation code
}
На момент написания этой статьи поддержка @scroll-timeline
браузером отсутствует. Это черновая спецификация, содержащая только экспериментальные реализации. Однако у него есть полифилл, который я использую в этой демонстрации.
ScrollTimeline
Хотя CSS и JavaScript могут создавать временные шкалы прокрутки, я выбрал JavaScript, чтобы иметь возможность использовать измерения элементов в анимации в реальном времени.
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection, // snap-tabs > section
orientation: 'inline', // scroll in the direction letters flow
fill: 'both', // bi-directional linking
});
Я хочу, чтобы одна вещь следовала за позицией прокрутки другой, и, создавая ScrollTimeline
я определяю драйвер ссылки прокрутки, scrollSource
. Обычно анимация в Интернете выполняется в соответствии с глобальным интервалом времени, но с помощью специальной sectionScrollTimeline
в памяти я могу все это изменить.
tabindicator.animate({
transform: ...,
width: ...,
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Прежде чем я перейду к ключевым кадрам анимации, я думаю, важно отметить, что последователь прокрутки, tabindicator
, будет анимироваться на основе пользовательской временной шкалы — прокрутки нашего раздела. На этом связь завершена, но отсутствует последний ингредиент — точки с сохранением состояния для анимации между ними, также известные как ключевые кадры.
Динамические ключевые кадры
Существует действительно мощный чисто декларативный CSS-способ анимации с помощью @scroll-timeline
, но анимация, которую я выбрал, была слишком динамичной. Невозможно переключаться между auto
шириной и нет способа динамически создавать несколько ключевых кадров в зависимости от длины дочерних элементов.
Однако JavaScript знает, как получить эту информацию, поэтому мы сами будем перебирать дочерние элементы и получать вычисленные значения во время выполнения:
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Для каждого tabnavitem
деструктурируйте позицию offsetLeft
и верните строку, которая использует ее в качестве значения translateX
. Это создаст 4 ключевых кадра преобразования для анимации. То же самое делается и с шириной: каждому задается вопрос, какова его динамическая ширина, а затем она используется в качестве значения ключевого кадра.
Вот пример вывода, основанный на моих шрифтах и настройках браузера:
Ключевые кадры TranslateX:
[...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`)
// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]
Ширина ключевых кадров:
[...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]
Подводя итог стратегии, индикатор табуляции теперь будет анимироваться по 4 ключевым кадрам в зависимости от положения привязки прокрутки прокрутки раздела. Точки привязки создают четкое разграничение между ключевыми кадрами и действительно добавляют ощущения синхронизации анимации.
Пользователь управляет анимацией своим взаимодействием, видя, как ширина и положение индикатора меняются от одного раздела к другому, идеально отслеживая прокрутку.
Возможно, вы не заметили, но я очень горжусь переходом цвета при выборе выделенного элемента навигации.
Невыбранный светло-серый цвет кажется еще более отодвинутым назад, когда выделенный элемент становится более контрастным. Обычно изменение цвета текста происходит, например, при наведении курсора мыши и при выделении, но на следующем уровне происходит изменение цвета при прокрутке, синхронизированное с индикатором подчеркивания.
Вот как я это сделал:
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
});
Для каждой ссылки навигации по вкладкам требуется новая цветовая анимация, отслеживающая ту же временную шкалу прокрутки, что и индикатор подчеркивания. Я использую ту же временную шкалу, что и раньше: поскольку ее роль заключается в создании тика при прокрутке, мы можем использовать этот тик в любом типе анимации, который нам нужен. Как и раньше, я создаю в цикле 4 ключевых кадра и возвращаю цвета.
[...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
// results in 4 array items, which represent 4 keyframe states
// [
"var(--text-active-color)",
"var(--text-color)",
"var(--text-color)",
"var(--text-color)",
]
Ключевой кадр с цветом var(--text-active-color)
выделяет ссылку, в остальном это стандартный цвет текста. Вложенный цикл делает это относительно простым, поскольку внешний цикл — это каждый элемент навигации, а внутренний цикл — персональные ключевые кадры каждого элемента навигации. Я проверяю, совпадает ли элемент внешнего цикла с элементом внутреннего цикла, и использую это, чтобы узнать, когда он выбран.
Мне было очень весело писать это. Так много.
Еще больше улучшений JavaScript
Стоит напомнить, что суть того, что я вам здесь показываю, работает без JavaScript. С учетом сказанного, давайте посмотрим, как мы можем улучшить его, когда будет доступен JS.
Глубокие ссылки
Глубокие ссылки — это скорее мобильный термин, но я думаю, что цель глубокой ссылки здесь достигается с помощью вкладок, поскольку вы можете поделиться URL-адресом непосредственно с содержимым вкладки. Браузер на странице перейдет к идентификатору, который соответствует хешу URL-адреса. Я обнаружил, что этот обработчик onload
оказывает влияние на разные платформы.
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
Синхронизация завершения прокрутки
Наши пользователи не всегда щелкают мышью или используют клавиатуру, иногда они просто свободно прокручивают страницу, как и должно быть. Когда скроллер раздела перестанет прокручиваться, то, где бы он ни находился, должно быть сопоставлено с верхней панелью навигации.
Вот как я жду окончания прокрутки: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });
Всякий раз, когда разделы прокручиваются, очистите тайм-аут раздела, если он есть, и начните новый. Когда разделы перестают прокручиваться, не очищайте тайм-аут и запускайте 100 мс после отдыха. Когда он сработает, вызовите функцию, которая пытается выяснить, где остановился пользователь.
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth;
const matchingNavItem = tabnavitems[i];
matchingNavItem && setActiveTab(matchingNavItem);
};
Предполагая, что прокрутка зафиксирована, деление текущей позиции прокрутки на ширину области прокрутки должно дать целое число, а не десятичное число. Затем я пытаюсь получить navitem из нашего кэша через этот рассчитанный индекс, и если он что-то находит, я отправляю совпадение, чтобы оно стало активным.
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active');
tabbtn.setAttribute('active', '');
tabbtn.scrollIntoView();
};
Установка активной вкладки начинается с очистки любой активной в данный момент вкладки, а затем присвоения входящему элементу навигации атрибута активного состояния. Вызов scrollIntoView()
имеет забавное взаимодействие с CSS, на которое стоит обратить внимание.
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
В CSS-утилиту привязки горизонтальной прокрутки мы вложили медиа-запрос, который применяет smooth
прокрутку, если пользователь терпим к движению. JavaScript может свободно выполнять вызовы для прокрутки элементов в поле зрения, а CSS может декларативно управлять UX. Иногда они составляют очаровательную пару.
Заключение
Теперь, когда вы знаете, как я это сделал, как бы вы поступили?! Это создает забавную компонентную архитектуру! Кто собирается сделать 1-ю версию со слотами на любимом фреймворке? 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте глюк , напишите мне в Твиттере свою версию, и я добавлю ее в раздел ремиксов сообщества ниже.
Ремиксы сообщества
- @devnook , @rob_dodson и @DasSurma с веб-компонентами: статья .
- @jhvanderschee с кнопками: Codepen .
Базовый обзор того, как создать компонент вкладок, аналогичный тем, которые есть в приложениях для iOS и Android.
В этом посте я хочу поделиться мыслями о создании веб-компонента «Вкладки», который будет адаптивным, поддерживает ввод с нескольких устройств и работает во всех браузерах. Попробуйте демо .
Если вы предпочитаете видео, вот версия этого поста на YouTube:
Обзор
Вкладки являются распространенным компонентом дизайн-систем, но могут принимать самые разные формы. Сначала были вкладки рабочего стола, построенные на элементе <frame>
, а теперь у нас есть удобные мобильные компоненты, которые анимируют контент на основе физических свойств. Все они пытаются сделать одно и то же: сэкономить место.
Сегодня основой пользовательского опыта работы с вкладками является область навигации по кнопкам, которая переключает видимость контента в рамке дисплея. Множество различных областей контента занимают одно и то же пространство, но представлены условно в зависимости от кнопки, выбранной в навигации.
Веб-тактика
В целом этот компонент показался мне довольно простым в создании благодаря нескольким важным функциям веб-платформы:
-
scroll-snap-points
для элегантного взаимодействия с помощью смахивания и клавиатуры с соответствующими позициями остановки прокрутки - Глубокие ссылки через хэши URL-адресов для поддержки привязки прокрутки на странице и поддержки совместного использования браузером.
- Поддержка чтения с экрана с разметкой элементов
<a>
иid="#hash"
-
prefers-reduced-motion
для включения плавных переходов и мгновенной прокрутки страницы. - Встроенная веб-функция
@scroll-timeline
для динамического подчеркивания и изменения цвета выбранной вкладки.
HTML
По сути, UX здесь таков: щелкните ссылку, URL-адрес будет представлять состояние вложенной страницы, а затем вы увидите обновление области контента по мере прокрутки браузера к соответствующему элементу.
Здесь есть некоторые элементы структурного контента: ссылки и :target
. Нам нужен список ссылок, для которых отлично подходит <nav>
, и список элементов <article>
, для которых отлично подходит <section>
. Каждый хеш ссылки будет соответствовать разделу, позволяя браузеру прокручивать элементы посредством привязки.
Например, при нажатии на ссылку автоматически фокусируется статья :target
в Chrome 89, JS не требуется. Затем пользователь может, как всегда, прокручивать содержимое статьи с помощью своего устройства ввода. Как указано в разметке, это бесплатный контент.
Для организации вкладок я использовал следующую разметку:
<snap-tabs>
<header>
<nav>
<a></a>
<a></a>
<a></a>
<a></a>
</nav>
</header>
<section>
<article></article>
<article></article>
<article></article>
<article></article>
</section>
</snap-tabs>
Я могу установить связи между элементами <a>
и <article>
с помощью свойств href
и id
следующим образом:
<snap-tabs>
<header>
<nav>
<a href="#responsive"></a>
<a href="#accessible"></a>
<a href="#overscroll"></a>
<a href="#more"></a>
</nav>
</header>
<section>
<article id="responsive"></article>
<article id="accessible"></article>
<article id="overscroll"></article>
<article id="more"></article>
</section>
</snap-tabs>
Затем я наполнил статьи разным количеством лоремов, а ссылки разной длиной и набором заголовков. Имея контент для работы, мы можем приступить к верстке.
Прокрутка макетов
В этом компоненте есть 3 различных типа областей прокрутки:
- Навигация (розовая) с возможностью горизонтальной прокрутки.
- Область содержимого (синяя) прокручивается по горизонтали.
- Каждый элемент статьи (зеленый) прокручивается по вертикали.
Существует два разных типа элементов, участвующих в прокрутке:
- Окно
Поле с определенными размерами, имеющее стиль свойстваoverflow
. - Негабаритная поверхность
В этом макете это контейнеры списков: навигационные ссылки, статьи разделов и содержимое статей.
Макет <snap-tabs>
Макет верхнего уровня, который я выбрал, был гибким (Flexbox). Я установил направление column
, чтобы заголовок и раздел располагались вертикально. Это наше первое окно прокрутки, и оно скрывает все, а переполнение скрыто. В заголовке и разделе скоро будет использоваться прокрутка, как в отдельных зонах.
<snap-tabs> <header></header> <section></section> </snap-tabs>
snap-tabs { display: flex; flex-direction: column; /* establish primary containing box */ overflow: hidden; position: relative; & > section { /* be pushy about consuming all space */ block-size: 100%; } & > header { /* defend againstneeding 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }
Возвращаясь к красочной диаграмме с тремя прокрутками:
-
<header>
теперь готов стать (розовым) контейнером прокрутки. -
<section>
подготовлен как (синий) контейнер прокрутки.
Кадры, которые я выделил ниже с помощью VisBug, помогают нам увидеть окна, созданные контейнерами прокрутки.
Макет вкладок <header>
Следующий макет почти такой же: я использую flex для создания вертикального порядка.
<snap-tabs> <header> <nav></nav> <span class="snap-indicator"></span> </header> <section></section> </snap-tabs>
header { display: flex; flex-direction: column; }
.snap-indicator
должен перемещаться горизонтально вместе с группой ссылок, и этот макет заголовка помогает подготовить этот этап. Здесь нет абсолютно позиционированных элементов!
Далее стили прокрутки. Оказывается, мы можем использовать стили прокрутки для двух наших горизонтальных областей прокрутки (заголовок и раздел), поэтому я создал служебный класс .scroll-snap-x
.
.scroll-snap-x {
/* browser decide if x is ok to scroll and show bars on, y hidden */
overflow: auto hidden;
/* prevent scroll chaining on x scroll */
overscroll-behavior-x: contain;
/* scrolling should snap children on x */
scroll-snap-type: x mandatory;
@media (hover: none) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
}
Каждому из них требуется переполнение по оси X, ограничение прокрутки для предотвращения чрезмерной прокрутки, скрытые полосы прокрутки для сенсорных устройств и, наконец, привязка прокрутки для блокировки областей представления контента. Наш порядок вкладок на клавиатуре доступен, и любые взаимодействия естественным образом направляют фокус. Контейнеры с привязкой к прокрутке также имеют удобное взаимодействие с клавиатурой в стиле карусели.
Макет заголовка вкладок <nav>
Навигационные ссылки должны располагаться в линию без разрывов строк, по центру по вертикали, и каждый элемент ссылки должен быть привязан к контейнеру привязки к прокрутке. Swift работает для CSS 2021 года!
<nav> <a></a> <a></a> <a></a> <a></a> </nav>
nav { display: flex; & a { scroll-snap-align: start; display: inline-flex; align-items: center; white-space: nowrap; } }
Каждая ссылка имеет собственный стиль и размер, поэтому в макете навигации необходимо указать только направление и поток. Уникальная ширина элементов навигации делает переход между вкладками увлекательным, поскольку индикатор подстраивает свою ширину под новую цель. В зависимости от того, сколько здесь элементов, браузер будет отображать полосу прокрутки или нет.
Макет вкладок <section>
Этот раздел является гибким элементом и должен быть доминирующим потребителем пространства. Также необходимо создать столбцы для размещения статей. И снова быстрая работа над CSS 2021! block-size: 100%
растягивает этот элемент, чтобы максимально заполнить родительский элемент, затем для собственного макета он создает серию столбцов, ширина которых составляет 100%
ширины родительского элемента. Проценты здесь отлично работают, потому что мы наложили строгие ограничения на родительский элемент.
<section> <article></article> <article></article> <article></article> <article></article> </section>
section { block-size: 100%; display: grid; grid-auto-flow: column; grid-auto-columns: 100%; }
Это как если бы мы говорили: «Расширяйтесь по вертикали настолько, насколько это возможно, настойчиво» (помните заголовок, который мы установили для flex-shrink: 0
: это защита от этого расширения), который устанавливает высоту строки для набор колонн во всю высоту. Стиль auto-flow
предписывает сетке всегда располагать дочерние элементы в горизонтальной линии, без переноса, именно так, как мы хотим; для переполнения родительского окна.
Мне иногда трудно уложить в голове эти мысли! Этот элемент раздела вписывается в коробку, но также создает набор коробок. Надеюсь, иллюстрации и пояснения помогут.
Макет вкладок <article>
Пользователь должен иметь возможность прокручивать содержимое статьи, а полосы прокрутки должны отображаться только в случае переполнения. Эти элементы статьи находятся в аккуратном положении. Они одновременно являются родительским элементом прокрутки и дочерним элементом прокрутки. Здесь браузер действительно справляется с некоторыми сложными взаимодействиями с сенсорным экраном, мышью и клавиатурой.
<article> <h2></h2> <p></p> <p></p> <h2></h2> <p></p> <p></p> ... </article>
article { scroll-snap-align: start; overflow-y: auto; overscroll-behavior-y: contain; }
Я решил, чтобы статьи прикреплялись к родительскому скроллеру. Мне очень нравится, как элементы навигационных ссылок и элементы статьи привязываются к началу строки соответствующих контейнеров прокрутки. Это выглядит и ощущается как гармоничные отношения.
Статья является дочерним элементом сетки, и ее размер заранее определен как область просмотра, которую мы хотим предоставить для прокрутки. Это означает, что мне не нужны стили высоты или ширины, мне просто нужно определить, как происходит переполнение. Я установил для overflow-y значение auto, а затем также перехватываю взаимодействие прокрутки с помощью удобного свойства overscroll-behavior.
Обзор трех областей прокрутки
Ниже я выбрал в настройках своей системы «всегда показывать полосы прокрутки». Я думаю, что для макета вдвойне важно работать с включенным этим параметром, поскольку мне приходится проверять макет и оркестровку прокрутки.
Я думаю, что наличие поля полосы прокрутки в этом компоненте помогает четко показать, где находятся области прокрутки, какое направление они поддерживают и как они взаимодействуют друг с другом. Рассмотрим, что каждый из этих фреймов окон прокрутки также является гибким или сеточным родительским элементом макета.
DevTools может помочь нам визуализировать это:
Макеты прокрутки готовы: привязка, глубокая связь и доступ с клавиатуры. Прочная основа для улучшения UX, стиля и удовольствия.
Основные характеристики
Дочерние элементы, привязанные к прокрутке, сохраняют свое заблокированное положение во время изменения размера. Это означает, что JavaScript не нужно будет отображать что-либо при повороте устройства или изменении размера браузера. Попробуйте это в режиме устройства Chromium DevTools, выбрав любой режим, кроме Responsive , а затем изменив размер рамки устройства. Обратите внимание, что элемент остается в поле зрения и заблокирован своим содержимым. Это стало доступно с тех пор, как Chromium обновил свою реализацию, чтобы она соответствовала спецификации. Вот сообщение в блоге об этом.
Анимация
Цель работы по анимации — четко связать взаимодействие с обратной связью пользовательского интерфейса. Это помогает пользователю (надеюсь) беспрепятственно открыть весь контент. Я буду добавлять движение целенаправленно и условно. Теперь пользователи могут указывать свои предпочтения в движении в своей операционной системе, и мне очень нравится реагировать на их предпочтения в своих интерфейсах.
Я свяжу подчеркивание табуляции с позицией прокрутки статьи. Привязка не только обеспечивает хорошее выравнивание, но и закрепляет начало и конец анимации. Это сохраняет <nav>
, который действует как мини-карта , связанным с содержимым. Мы будем проверять предпочтения пользователя в отношении движения как с помощью CSS, так и с помощью JS. Есть несколько замечательных мест, на которые стоит обратить внимание!
Поведение прокрутки
Существует возможность улучшить поведение при движении как :target
, так и element.scrollIntoView()
. По умолчанию это мгновенно. Браузер просто устанавливает положение прокрутки. А что, если мы захотим перейти в эту позицию прокрутки, а не моргать там?
@media (prefers-reduced-motion: no-preference) {
.scroll-snap-x {
scroll-behavior: smooth;
}
}
Поскольку здесь мы представляем движение, а также движение, которое пользователь не контролирует (например, прокрутку), мы применяем этот стиль только в том случае, если у пользователя нет предпочтений в своей операционной системе в отношении уменьшения движения. Таким образом, мы вводим движение прокрутки только для тех, кого это устраивает.
Индикатор вкладок
Цель этой анимации — помочь связать индикатор с состоянием контента. Я решил раскрасить стили кроссфейдной border-bottom
для пользователей, которые предпочитают ограниченное движение, и анимацию скольжения и затухания со ссылкой на прокрутку для пользователей, которых устраивает движение.
В Chromium Devtools я могу переключить предпочтения и продемонстрировать два разных стиля перехода. Я получил массу удовольствия, создавая это.
@media (prefers-reduced-motion: reduce) {
snap-tabs > header a {
border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
transition: color .7s ease, border-color .5s ease;
&:is(:target,:active,[active]) {
color: var(--text-active-color);
border-block-end-color: hsl(var(--accent));
}
}
snap-tabs .snap-indicator {
visibility: hidden;
}
}
Я скрываю .snap-indicator
, когда пользователь предпочитает ограниченное движение, поскольку он мне больше не нужен. Затем я заменяю его стилями border-block-end
и transition
. Также обратите внимание при взаимодействии вкладок, что активный элемент навигации не только имеет подчеркивание бренда, но и цвет текста становится темнее. Активный элемент имеет более высокий цветовой контраст текста и яркий акцент подсветки.
Всего несколько дополнительных строк CSS заставят кого-то почувствовать себя увиденным (в том смысле, что мы внимательно относимся к его предпочтениям в движении). Я люблю это.
@scroll-timeline
В предыдущем разделе я показал вам, как я обрабатываю стили кроссфейда с уменьшенным движением, а в этом разделе я покажу вам, как я связал индикатор и область прокрутки вместе. Далее вас ждут забавные экспериментальные вещи. Надеюсь, вы так же взволнованы, как и я.
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
);
Сначала я проверяю предпочтения пользователя в отношении движения с помощью JavaScript. Если результатом этого является false
, что означает, что пользователь предпочитает ограниченное движение, мы не будем запускать какие-либо эффекты движения, связывающие прокрутку.
if (motionOK) {
// motion based animation code
}
На момент написания этой статьи поддержка @scroll-timeline
браузером отсутствует. Это черновая спецификация, содержащая только экспериментальные реализации. Однако у него есть полифилл, который я использую в этой демонстрации.
ScrollTimeline
Хотя CSS и JavaScript могут создавать временные шкалы прокрутки, я выбрал JavaScript, чтобы иметь возможность использовать измерения элементов в анимации в реальном времени.
const sectionScrollTimeline = new ScrollTimeline({
scrollSource: tabsection, // snap-tabs > section
orientation: 'inline', // scroll in the direction letters flow
fill: 'both', // bi-directional linking
});
Я хочу, чтобы одна вещь следовала за позицией прокрутки другой, и, создавая ScrollTimeline
я определяю драйвер ссылки прокрутки, scrollSource
. Обычно анимация в Интернете выполняется в соответствии с глобальным интервалом времени, но с помощью специальной sectionScrollTimeline
в памяти я могу все это изменить.
tabindicator.animate({
transform: ...,
width: ...,
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Прежде чем я перейду к ключевым кадрам анимации, я думаю, важно отметить, что последователь прокрутки, tabindicator
, будет анимироваться на основе пользовательской временной шкалы — прокрутки нашего раздела. На этом связь завершена, но отсутствует последний ингредиент — точки с сохранением состояния для анимации между ними, также известные как ключевые кадры.
Динамические ключевые кадры
Существует действительно мощный чисто декларативный CSS-способ анимации с помощью @scroll-timeline
, но анимация, которую я выбрал, была слишком динамичной. Невозможно переключаться между auto
шириной и нет способа динамически создавать несколько ключевых кадров в зависимости от длины дочерних элементов.
Однако JavaScript знает, как получить эту информацию, поэтому мы сами будем перебирать дочерние элементы и получать вычисленные значения во время выполнения:
tabindicator.animate({
transform: [...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`),
width: [...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
Для каждого tabnavitem
деструктурируйте позицию offsetLeft
и верните строку, которая использует ее в качестве значения translateX
. Это создаст 4 ключевых кадра преобразования для анимации. То же самое делается и с шириной: каждому задается вопрос, какова его динамическая ширина, а затем она используется в качестве значения ключевого кадра.
Вот пример вывода, основанный на моих шрифтах и настройках браузера:
Ключевые кадры TranslateX:
[...tabnavitems].map(({offsetLeft}) =>
`translateX(${offsetLeft}px)`)
// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]
Ширина ключевых кадров:
[...tabnavitems].map(({offsetWidth}) =>
`${offsetWidth}px`)
// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]
Подводя итог стратегии, индикатор табуляции теперь будет анимироваться по 4 ключевым кадрам в зависимости от положения привязки прокрутки прокрутки раздела. Точки привязки создают четкое разграничение между ключевыми кадрами и действительно добавляют ощущения синхронизации анимации.
Пользователь управляет анимацией своим взаимодействием, видя, как ширина и положение индикатора меняются от одного раздела к другому, идеально отслеживая прокрутку.
Возможно, вы не заметили, но я очень горжусь переходом цвета при выборе выделенного элемента навигации.
Невыбранный светло-серый цвет кажется еще более отодвинутым назад, когда выделенный элемент становится более контрастным. Обычно изменение цвета текста происходит, например, при наведении курсора мыши и при выделении, но на следующем уровне происходит изменение цвета при прокрутке, синхронизированное с индикатором подчеркивания.
Вот как я это сделал:
tabnavitems.forEach(navitem => {
navitem.animate({
color: [...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
}, {
duration: 1000,
fill: 'both',
timeline: sectionScrollTimeline,
}
);
});
Для каждой ссылки навигации по вкладкам требуется новая цветовая анимация, отслеживающая ту же временную шкалу прокрутки, что и индикатор подчеркивания. Я использую ту же временную шкалу, что и раньше: поскольку ее роль заключается в создании тика при прокрутке, мы можем использовать этот тик в любом типе анимации, который нам нужен. Как и раньше, я создаю в цикле 4 ключевых кадра и возвращаю цвета.
[...tabnavitems].map(item =>
item === navitem
? `var(--text-active-color)`
: `var(--text-color)`)
// results in 4 array items, which represent 4 keyframe states
// [
"var(--text-active-color)",
"var(--text-color)",
"var(--text-color)",
"var(--text-color)",
]
Ключевой кадр с цветом var(--text-active-color)
выделяет ссылку, в остальном это стандартный цвет текста. Вложенный цикл делает это относительно простым, поскольку внешний цикл — это каждый элемент навигации, а внутренний цикл — персональные ключевые кадры каждого элемента навигации. Я проверяю, совпадает ли элемент внешнего цикла с элементом внутреннего цикла, и использую это, чтобы узнать, когда он выбран.
Мне было очень весело писать это. Так много.
Еще больше улучшений JavaScript
Стоит напомнить, что суть того, что я вам здесь показываю, работает без JavaScript. С учетом сказанного, давайте посмотрим, как мы можем улучшить его, когда будет доступен JS.
Глубокие ссылки
Глубокие ссылки — это скорее мобильный термин, но я думаю, что цель глубокой ссылки здесь достигается с помощью вкладок, поскольку вы можете поделиться URL-адресом непосредственно с содержимым вкладки. Браузер на странице перейдет к идентификатору, который соответствует хешу URL-адреса. Я обнаружил, что этот обработчик onload
оказывает влияние на разные платформы.
window.onload = () => {
if (location.hash) {
tabsection.scrollLeft = document
.querySelector(location.hash)
.offsetLeft;
}
}
Синхронизация завершения прокрутки
Наши пользователи не всегда щелкают мышью или используют клавиатуру, иногда они просто свободно прокручивают страницу, как и должно быть. Когда скроллер раздела перестанет прокручиваться, то, где бы он ни находился, должно быть сопоставлено с верхней панелью навигации.
Вот как я жду окончания прокрутки: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });
Всякий раз, когда разделы прокручиваются, очистите тайм-аут раздела, если он есть, и начните новый. Когда разделы перестают прокручиваться, не очищайте тайм-аут и запускайте 100 мс после отдыха. Когда он сработает, вызовите функцию, которая пытается выяснить, где остановился пользователь.
const determineActiveTabSection = () => {
const i = tabsection.scrollLeft / tabsection.clientWidth;
const matchingNavItem = tabnavitems[i];
matchingNavItem && setActiveTab(matchingNavItem);
};
Предполагая, что прокрутка зафиксирована, деление текущей позиции прокрутки на ширину области прокрутки должно дать целое число, а не десятичное число. Затем я пытаюсь получить navitem из нашего кэша через этот рассчитанный индекс, и если он что-то находит, я отправляю совпадение, чтобы оно стало активным.
const setActiveTab = tabbtn => {
tabnav
.querySelector(':scope a[active]')
.removeAttribute('active');
tabbtn.setAttribute('active', '');
tabbtn.scrollIntoView();
};
Установка активной вкладки начинается с очистки любой активной в данный момент вкладки, а затем присвоения входящему элементу навигации атрибута активного состояния. Вызов scrollIntoView()
имеет забавное взаимодействие с CSS, на которое стоит обратить внимание.
.scroll-snap-x {
overflow: auto hidden;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
@media (prefers-reduced-motion: no-preference) {
scroll-behavior: smooth;
}
}
В CSS-утилиту привязки горизонтальной прокрутки мы вложили медиа-запрос, который применяет smooth
прокрутку, если пользователь терпим к движению. JavaScript может свободно выполнять вызовы для прокрутки элементов в поле зрения, а CSS может декларативно управлять UX. Иногда они составляют очаровательную пару.
Заключение
Теперь, когда вы знаете, как я это сделал, как бы вы поступили?! Это создает забавную компонентную архитектуру! Кто собирается сделать 1-ю версию со слотами на любимом фреймворке? 🙂
Давайте разнообразим наши подходы и изучим все способы разработки в Интернете. Создайте глюк , напишите мне в Твиттере свою версию, и я добавлю ее в раздел ремиксов сообщества ниже.
Ремиксы сообщества
- @devnook , @rob_dodson и @DasSurma с веб-компонентами: статья .
- @jhvanderschee с кнопками: Codepen .