TGViewer
<divelopers> <divelopers> @alexnozer_dev · 1.18K subscribers
Post #411 160
Управление маркерами CSS-каруселей

После релиза CSS-каруселей в Chrome 135 перед Google I/O в прошлом году и критики доступности, новостей о развитии этого API как-то не было. Но в Chrome 140 и Chrome 154 (и соответствующих версиях Edge) CSS-карусели были немного улучшены.

Я делал пост о CSS-каруселях. Напомню, что это возможность добавить контейнеру с прокруткой кнопки «вперёд» и «назад», а также группу с маркерами (пагинацию) с помощью CSS. В сочетании со Scroll Snap получаются карусели без JS.

<ul class="carousel">
<li data-slide="1">
<!-- Слайд 1 -->
</li>
<li data-slide="2">
<!-- Слайд 2 -->
</li>
<li data-slide="3">
<!-- Слайд 3 -->
</li>
</ul>
<style>
.carousel {
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-marker-group: after;
scrollbar-width: none;

[data-slide] {
scroll-snap-align: center;
}

[data-slide]::scroll-marker {
/* Резервное значение */
content: attr(data-slide);
content: sibling-index();
}

[data-slide]::scroll-marker:target-current {
background: var(--accent);
}

&::scroll-button(left) {
content: "←" / "Previous";
}

&::scroll-button(right) {
content: "→" / "Next";
}

&::scroll-button(*):focus-visible {
outline-offset: 5px;
}
}
</style>


Одна из претензий к CSS-каруселям — создание интерактивных элементов в CSS. Кнопки, маркеры и их обёртка создаются в виде интерактивных псевдо-элементов что не даёт полного контроля и принуждает использовать content в CSS.

При просмотре в DevTools у карусели из примера выше получается такая структура:

<ul class="carousel">
::scroll-button(left)
::scroll-button(right)
::scroll-marker-group
<li data-slide="1">
::scroll-marker
<!-- Слайд 1 -->
</li>
<li data-slide="2">
::scroll-marker
<!-- Слайд 2 -->
</li>
<li data-slide="3">
::scroll-marker
<!-- Слайд 3 -->
</li>
</ul>


Внутри списка создаются псевдо-элементы ::scroll-button(*) — кнопки «вперёд» и «назад», ::scroll-marker-group — контейнер для маркеров с ролью navigation и ::scroll-marker — сами маркеры с ролью link.

Эту структуру с псевдо-элементами можно выразить в виде HTML-эквивалента для понимания итоговой семантики и связей элементов. Кнопки выносятся перед списком слайдов, а контейнер с маркерами в виде <nav> и <a> после списка.

<button
aria-label="Previous"
aria-controls="[ссылается на <ul>]"
>
←
</button>
<button
aria-label="Next"
aria-controls="[ссылается на <ul>]"
>
→
</button>
<ul class="carousel">
<li data-slide="1">
<!-- Слайд 1 -->
</li>
<li data-slide="2">
<!-- Слайд 2 -->
</li>
<li data-slide="3">
<!-- Слайд 3 -->
</li>
</ul>
<nav aria-controls="[ссылается на <ul>]">
<a
href="[ссылается на <li data-slide='1'>]"
aria-details="[ссылается на <li data-slide='1'>]"
>
1
</a>
<a
href="[ссылается на <li data-slide='2'>]"
aria-details="[ссылается на <li data-slide='2'>]"
>
2
</a>
<a
href="[ссылается на <li data-slide='3'>]"
aria-details="[ссылается на <li data-slide='3'>]"
>
3
</a>
</nav>


По умолчанию контейнер с маркерами получает роль navigation, а сами маркеры — роль link. Работает это, соответственно, как <nav> с набором <a> внутри. Изменить это поведение можно с помощью новых значений scroll-marker-group.

Значения links и tabs идут в дополнение к уже существующим значениям before и after, которые определяют визуальное положение группы маркеров. links и tabs определяют тип группы:

- links: у контейнера маркеров будет роль navigation, у маркеров будет роль link и они будут вести себя как якорные ссылки, табуляция по ссылкам будет последовательная;

- tabs: у контейнера маркеров будет роль tablist, у маркеров будет роль tab и они будут вести себя как вкладки, табуляция попадает только на активную вкладку, а навигация между вкладками с помощью стрелок.

<ul class="carousel">
<li data-slide="1">
<!-- Слайд 1 -->
</li>
<li data-slide="2">
<!-- Слайд 2 -->
</li>
<li data-slide="3">
<!-- Слайд 3 -->
</li>
</ul>

<style>
.carousel {
/* ... */
scroll-marker-group: after tabs;
}
</style>

<!-- Итоговая структура в HTML-эквиваленте -->
<ul class="carousel">
<li data-slide="1">
<!-- Слайд 1 -->
</li>
<li data-slide="2">
<!-- Слайд 2 -->
</li>
<li data-slide="3">
<!-- Слайд 3 -->
</li>
</ul>
<div
role="tablist"
aria-multiselectable="false"
aria-orientation="horizontal"
aria-controls="[ссылается на <ul>]"
>
<button
role="tab"
aria-selected="true"
aria-details="[ссылается на <li data-slide='1'>]"
>
1
</button>
<button
role="tab"
aria-selected="false"
aria-details="[ссылается на <li data-slide='2'>]"
>
2
</button>
<button
role="tab"
aria-selected="false"
aria-details="[ссылается на <li data-slide='3'>]"
>
3
</button>
</div>

Контейнер меняет роль с navigation на tablist, а маркеры с link на tab. Так как tablist — это виджет, который требует клавиатурной навигации, браузер автоматически добавляет навигацию стрелками между маркерами.

Второй способ управления группой маркеров — возможность выноса в HTML. У элемента внутри карусели можно указать свойство scroll-target-group: auto и тогда он будет использоваться в качестве контейнера для маркеров.

Чтобы это работало аналогично группе в CSS, внутри контейнера должны быть якорные ссылки, которые станут аналогом псевдо-элементов ::scroll-marker. Для них браузер автоматически вычислит состояние :target-current.

<div class="carousel">
<div id="slide-1">
<!-- Слайд 1 -->
</div>
<div id="slide-2">
<!-- Слайд 2 -->
</div>
<div id="slide-3">
<!-- Слайд 3 -->
</div>
<nav class="markers">
<a href="#slide-1">1</a>
<a href="#slide-2">2</a>
<a href="#slide-3">3</a>
</nav>
</div>
<style>
.carousel {
/* ... */

.markers {
scroll-target-group: auto;

a:target-current {
background-color: var(--accent);
}
}
}
</style>


По большей части это предназначено для интерактивного содержания, а не карусели. Маркеры по шаблону карусели должны быть сочетанием tablist и tab. Если вместо ссылок будут другие элементы, то автоматика браузера не сработает.

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

<div class="carousel" id="carousel">
<!-- Слайды -->
<nav
class="markers"
focusgroup="tablist"
aria-controls="carousel"
>
<a
href="#slide-1"
role="tab"
aria-controls="slide-1"
aria-details="slide-1"
>
1
</a>
<!-- остальные маркеры -->
</nav>
</div>


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

#css #ui
  • 👍 4
More from @alexnozer_dev
  1. Sep 28, 2026You don’t know HTML: SGML, HTML, XML, XHTML За свою историю язык разметки гипертекста HTML…
  2. Sep 23, 2026Images Preview На некоторых сайтах, где надо показывать изображения в высоком качестве, дл…
  3. Sep 21, 2026Реализация пользовательских атрибутов Под конец прошлого года я рассказывал об API пользов…
  4. Sep 14, 2026Подклассы Event вместо CustomEvent Многие библиотеки предоставляют систему событий в качес…
  5. Sep 11, 2026Эволюция стилей в темах Shopify Адам Ватан на днях поделился новостью, что Shopify выкупил…
  6. Sep 9, 2026Persistent Widgets Многие сайты не должны быть SPA. Но иногда эта архитектура продиктована…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →