惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
Recent Commits to openclaw:main
Recent Commits to openclaw:main
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
罗磊的独立博客
V
Visual Studio Blog
爱范儿
爱范儿
H
Help Net Security
J
Java Code Geeks
I
InfoQ
Recent Announcements
Recent Announcements
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Recorded Future
Recorded Future
Jina AI
Jina AI
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
GbyAI
GbyAI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Y
Y Combinator Blog
Google DeepMind News
Google DeepMind News
Scott Helme
Scott Helme
S
SegmentFault 最新的问题
S
Securelist
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
C
Cisco Blogs
Simon Willison's Weblog
Simon Willison's Weblog
G
Google Developers Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 叶小钗
T
The Blog of Author Tim Ferriss
博客园_首页
B
Blog
F
Fortinet All Blogs
AWS News Blog
AWS News Blog
V
Vulnerabilities – Threatpost
S
Secure Thoughts
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Forbes - Security
Forbes - Security
S
Security @ Cisco Blogs
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Schneier on Security
Project Zero
Project Zero
Martin Fowler
Martin Fowler
C
Cybersecurity and Infrastructure Security Agency CISA
N
Netflix TechBlog - Medium
N
News and Events Feed by Topic

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Управляемый прокруткой CSS в 2026 году — разработка карусели без JavaScript
Igor Agapov · 2026-05-14 · via Все публикации подряд на Хабре

Средний

18 мин

11K

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

  • Структурируйте HTML с помощью семантических элементов <figure> внутри прокручиваемого контейнера с role="region".

  • Примените scroll-snap-type: x mandatory к контейнеру и scroll-snap-align: center к каждому элементу.

  • Сгенерируйте кнопки переключения слайдов с помощью псевдоэлементов ::scroll-button(left) и ::scroll-button(right).

  • Создайте точечные индикаторы с помощью ::scroll-marker() на каждом элементе и стилизуйте их активное состояние с помощью :checked.

  • Привяжите анимацию появления слайда к положению прокрутки с помощью animation-timeline: view(inline) и animation-range.

  • Добавьте индикатор прогресса с помощью именованного scroll-timeline, привязанного к псевдоэлементу ::before контейнера.

  • Поместите все новые возможности в блоки @supports, чтобы браузеры без их поддержки могли “откатиться” к прокручиваемому контейнеру.

  • В целях обеспечения доступности используйте @media (prefers-reduced-motion: reduce) для отключения анимации.

Конец эпохи каруселей, реализованных с помощью JavaScript

Исторически создание карусели в веб-разработке означало использование какой-либо JS-библиотеки. Slick, Swiper, Flickity, Embla Carousel и другие заполняли пробел платформы. Каждая из них увеличивала размер сборки, имела свои особенности обеспечения доступности и накладные расходы на обработку событий. Полный минифицированный пакет Swiper v11 весит приблизительно 140 КБ; даже модульные импорты составляют 40-60 КБ для ядра и навигации. Умножьте это на бесчисленные маркетинговые страницы, галереи товаров в электронной коммерции и макеты с большим количеством медиаконтента, которые зависят от каруселей, и расходы сильно возрастут: долгое время до интерактивности, блокировка основного потока во время инициализации и большой размер передаваемых данных на каждой странице, где используется слайдер.

Ситуация с доступностью еще хуже. Карусели, управляемые JS, нередко игнорируют навигацию с помощью клавиатуры, не отображают правильную семантику ARIA и непредсказуемым образом перехватывают фокус. Только самые добросовестные авторы библиотек добавили поддержку программ чтения с экрана. Остальные оставили это без внимания.

Спецификация CSS Overflow Level 5 представила псевдоэлементы ::scroll-button() и ::scroll-marker(). Спецификация Scroll-Driven Animations Level 1 обеспечила широкую, но пока не универсальную поддержку движков; поддержка Firefox остается частичной по состоянию на середину 2026 года. Вместе эти API позволяют создавать полностью функциональные и доступные карусели без использования JS.

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

Предварительные требования и поддержка браузеров

Что нужно знать

Предполагается средний уровень владения CSS, хорошие знания пользовательских свойств, псевдоэлементов и компоновки Grid/Flexbox. Знание свойств scroll-snap полезно, но не обязательно, поскольку их основы рассматриваются в статье.

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

По состоянию на середину 2026 года API для каруселей с прокруткой пользуются широкой, но не полной поддержкой.

Браузеры на основе Chromium (Chrome 135+, Edge 135+) предоставляют полную поддержку ::scroll-button(), ::scroll-marker(), ::scroll-marker-group и animation-timeline: scroll()/view(). Safari 19+ также обеспечивает полную поддержку. Firefox предлагает частичную поддержку за флагом: scroll-snap и animation-timeline работают, но ::scroll-button() и ::scroll-marker() на момент написания статьи остаются за флагом layout.css.scroll-driven-animations.enabled (проверено в Firefox 130; название флага может отличаться в более ранних или поздних версиях). Без флага пользователи Firefox получают прокручиваемый контейнер без элементов управления навигацией.

Правильная стратегия - прогрессивное улучшение. В неподдерживающих браузерах карусель превращается в горизонтально прокручиваемый контейнер. Блоки @supports, показанные ниже, обрабатывают это без проблем. На страницах Can I Use для “CSS Scroll-Driven Animations” и “CSS scroll-snap” представлены актуальные матрицы совместимости.

Понимание ключевых свойств CSS API, управляемых прокруткой

scroll-snap-type и scroll-snap-align

Привязка к прокрутке - основа любой CSS-карусели. Применение scroll-snap-type к прокручиваемому контейнеру определяет ось привязки и строгость. Применение scroll-snap-align к дочерним элементам определяет, где каждый элемент должен привязываться внутри области просмотра контейнера.

<div class="carousel">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
  <div class="item">4</div>
</div>
.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 1rem;
}

.item {
  flex: 0 0 80%;
  scroll-snap-align: center;
}

Установка mandatory заставляет контейнер докручивать слайд до следующей точки привязки после завершения прокрутки. Выравнивание center означает, что каждый элемент располагается по центру видимой области. Эта комбинация обеспечивает дискретное, “послайдовое” движение, определяющее поведение карусели.

::scroll-button(left) и ::scroll-button(right) - нативная навигация

Этот псевдоэлемент генерирует интерактивные элементы управления навигацией прямо из прокручиваемого контейнера, без элементов <button> в разметке. Браузер создает их как доступные, фокусируемые элементы управления, которые прокручивают контейнер по одной “странице” в заданном направлении при активации.

Этот псевдоэлемент принимает четыре значения направления: left, right, up и down. Для горизонтальной карусели ::scroll-button(left) и ::scroll-button(right) производят кнопки “Назад/вперед”, соответственно. Для отображения псевдоэлемента требуется значение content. Это свойство принимает текст, изображения CSS или URI данных SVG.

Важная особенность поведения: браузер автоматически отключает кнопку прокрутки при достижении контейнером соответствующей границы прокрутки. При инициализации карусели ::scroll-button(left) находится в отключенном состоянии. В конце отключается ::scroll-button(right). Можно стилизовать это состояние, исключив логику ручной проверки границ прокрутки, которую обычно используют JS-карусели.

Для генерации псевдоэлементов контейнер прокрутки должен иметь overflow: auto или overflow: scroll.

::scroll-marker-group и ::scroll-marker() - нативные точечные индикаторы

Применение ::scroll-marker() к потомкам контейнера прокрутки приводит к генерации точечного индикатора (или любого стилизованного маркера) для каждого потомка. Скобки в ::scroll-marker() - это часть синтаксиса функции псевдоэлемента; в текущей спецификации функция не принимает аргументы. Браузер группирует эти маркеры в псевдоэлемент ::scroll-marker-group на самом контейнере.

Установка display: flex в ::scroll-marker-group приводит к выстраиванию маркеров в строку. Добавление justify-content: center центрирует строку с точками. Положением этой строки можно управлять с помощью стандартных свойств позиционирования или order.

Браузер управляет активным состоянием автоматически. Маркер, соответствующий текущему привязанному элементу, получает псевдокласс :checked. Клик по любому маркеру прокручивает контейнер к соответствующему элементу. Добавление scroll-behavior: smooth к контейнеру приводит к включению анимации прокрутки. Это заменяет обработчики кликов, наблюдатели пересечений и управление состоянием, которые используют библиотеки JS-каруселей для синхронизации точечных индикаторов с позицией прокрутки.

animation-timeline: scroll() и animation-timeline: view() - анимации, управляемые прокруткой

Анимации, управляемые прокруткой, отличаются от других анимаций CSS тем, что они привязываются не ко времени, а к прогрессу прокрутки. Два типа временной шкалы служат разным целям.

В случае animation-timeline: scroll(), анимация отслеживает общий прогресс прокрутки контейнера. Когда контейнер прокручен на 50%, анимация завершена на 50%. Это хорошо подходит для индикаторов прогресса и эффектов на уровне контейнера.

В случае animation-timeline: view(), анимация отслеживает видимость элемента в области просмотра контейнера прокрутки. Анимация выполняется при входе, пересечении и выходе элемента из видимой области. Это хорошо подходит для эффектов на уровне элемента, таких как затухание/появление, масштабирование и параллакс.

animation-range используется для ограничения того, какая часть временной шкалы управляет анимацией. Например, animation-range: entry 0% entry 100% ограничивает анимацию стадией входа, от момента, когда первая часть элемента становится видимой, до момента, когда он становится полностью видимым. Первый видимый элемент при загрузке страницы может сразу находиться в состоянии to, поскольку он уже прошел стадию входа; используйте animation-fill-mode или начальные правила прозрачности/масштабирования для обработки этого крайнего случая. В режиме письма “слева-направо” ключевое слово inline в view(inline) определяет горизонтальную (строчную) ось для горизонтальных контейнеров прокрутки.

Шаг 1 - построение структуры HTML

Семантическая разметка для доступной карусели

Нам не нужны элементы <button> для навигации или обертка <nav> для точек. Эти элементы управления генерируются псевдоэлементами CSS. Разметка содержит только элементы <figure> внутри контейнера.

<div class="carousel" role="region" aria-label="Featured images">
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/1/800/300" alt="Mountain landscape at sunrise" />
    <figcaption>Mountain Sunrise</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/2/800/300" alt="Ocean waves crashing on rocks" />
    <figcaption>Coastal Waves</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/3/800/300" alt="Dense forest canopy from below" />
    <figcaption>Forest Canopy</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/4/800/300" alt="Desert dunes under a clear sky" />
    <figcaption>Desert Dunes</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/5/800/300" alt="Snow-covered mountain peak" />
    <figcaption>Alpine Peak</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/6/800/300" alt="Wildflower meadow in spring" />
    <figcaption>Spring Meadow</figcaption>
  </figure>
</div>

Атрибуты role="region" и aria-label определяют карусель как ориентир для вспомогательных технологий. Каждый элемент <figure> со своими <img> и <figcaption> предоставляет самодостаточный, семантически значимый контент.

Шаг 2 - реализация привязки прокрутки и макета

Плиточный трек карусели

Контейнер карусели использует grid-auto-flow: column для горизонтального расположения элементов. Каждый элемент масштабируется для заполнения всей ширины контейнера, оставляя видимыми части соседних элементов для обозначения возможности прокрутки.

Привязка к прокрутке

Установка scroll-snap-type: x mandatory к контейнеру обеспечивает четкую привязку слайдов друг к другу. scroll-snap-align: center центрирует каждый элемент после завершения жеста прокрутки.

.carousel {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 85%;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: auto;
  padding: 1rem;
  position: relative;
  box-sizing: border-box;
  scroll-timeline: --carousel inline;
}

.carousel::-webkit-scrollbar {
  display: none;
}

.carousel-item {
  scroll-snap-align: center;
  border-radius: 0.75rem;
  overflow: clip;
  overflow-clip-margin: 0px;
  margin: 0;
}

.carousel-item img {
  width: 100%;
  height: 300px;
  object-fit: cover;
  display: block;
}

.carousel-item figcaption {
  padding: 0.75rem 1rem;
  font-size: 1rem;
  background: #111;
  color: #fff;
}

scrollbar-width: auto сохраняет встроенную полосу прокрутки в качестве средства навигации в браузерах, которые не поддерживают ::scroll-button(). Когда эти псевдоэлементы доступны, полоса прокрутки скрывается внутри блока @supports (см. раздел, посвященный прогрессивному улучшению). box-sizing: border-box гарантирует, что grid-auto-columns: 85% и padding: 1rem разрешаются предсказуемо, без переполнения элементов или неправильного масштабирования. Использование overflow: clip на .carousel-item (вместо overflow: hidden) предотвращает визуальное переполнение из-за скругления границ без обрезки дочерних элементов figcaption при движении по оси X (translateX), используемых для параллаксной анимации. grid-auto-columns: 85% гарантирует, что каждый элемент занимает 85% ширины контейнера, оставляя края соседних элементов видимыми для обозначения возможности прокрутки. scroll-behavior: smooth гарантирует, что программная и запускаемая маркером прокрутка анимируется, а не происходит мгновенно. scroll-timeline: --carousel inline создает именованную временную шкалу прокрутки в контейнере для использования анимацией индикатора прогресса.

Шаг 3 - добавление навигации с помощью ::scroll-button()

Генерация кнопок “Назад/вперед”

Эти кнопки генерируются путем применения ::scroll-button(left) и ::scroll-button(right) к контейнеру .carousel. Для отображения каждого псевдоэлемента требуется свойство content.

Стилизация кнопок прокрутки

@supports selector(::scroll-button(right)) {
  .carousel::scroll-button(left),
  .carousel::scroll-button(right) {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    font-size: 1.25rem;
    display: grid;
    place-content: center;
    cursor: pointer;
    border: 2px solid rgba(255, 255, 255, 0.3);
    transition: background 0.2s ease, opacity 0.2s ease;
  }

  .carousel::scroll-button(left) {
    content: "◀";
    left: 0.5rem;
  }

  .carousel::scroll-button(right) {
    content: "▶";
    right: 0.5rem;
  }

  .carousel::scroll-button(left):hover,
  .carousel::scroll-button(right):hover {
    background: rgba(0, 0, 0, 0.85);
  }

  .carousel::scroll-button(left):disabled,
  .carousel::scroll-button(right):disabled {
    opacity: 0.3;
    cursor: default;
  }
}

Согласно черновику спецификации CSS Pseudo-Elements Level 5 псевдокласс :disabled автоматически применяется при достижении каруселью границ прокрутки. Нет необходимости в обнаружении этих границ с помощью JS. Переход (transition) прозрачности и фона предоставляет визуальный отклик на наведение курсора и изменение состояния. Защитник @supports selector(::scroll-button(right)) предотвращает ошибки парсинга в неподдерживающих браузерах.

Шаг 4 - добавление точечных индикаторов с помощью ::scroll-marker()

Генерация маркеров на основе элементов карусели

Каждый .carousel-item получает псевдоэлемент ::scroll-marker(). Свойство content создает видимую точку.

Позиционирование группы маркеров

Псевдоэлемент контейнера ::scroll-marker-group содержит все сгенерированные маркеры и стилизован как выровненная по центру флекс-строка.

Стилизация активных/неактивных состояний

@supports selector(::scroll-marker()) {
  .carousel::scroll-marker-group {
    display: flex;
    justify-content: center;
    gap: 0.5rem;
    padding-top: 1rem;
    position: relative;
    bottom: 0;
  }

  .carousel-item::scroll-marker() {
    content: "";
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.25);
    border: 2px solid transparent;
    transition: background 0.3s ease, transform 0.3s ease;
    cursor: pointer;
  }

  .carousel-item::scroll-marker():checked {
    background: #0070f3;
    transform: scale(1.3);
    border-color: #0070f3;
  }
}

Псевдокласс :checked на ::scroll-marker() отражает текущий привязанный элемент. Обратите внимание на скобки в ::scroll-marker():checked - только такой составной селектор считается валидным. Когда пользователь кликает по неактивной точке браузер выполняет прокрутку к соответствующему элементу карусели (с анимацией при установке scroll-behavior: smooth на контейнере). transition свойств background и transform длительностью 300 мс создает эффект плавного перехода между состояниями точки.

Шаг 5 - эффекты анимации, управляемой прокруткой

Появление и масштабирование при прокрутке с помощью view()

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

@keyframes fade-scale-in {
  from {
    opacity: 0.3;
    transform: scale(0.9);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* Первый элемент виден при загрузке страницы; рендерим его в финальном состоянии */
.carousel-item:first-child {
  animation: none;
  opacity: 1;
  transform: scale(1);
}

Временная шкала view(inline) отслеживает горизонтальную видимость каждого элемента. animation-range: entry 0% entry 100% ограничивает анимацию стадией входа. Когда элемент прокручивается в область просмотра с любой стороны, он переходит от 30% прозрачности и 90% размера к полной видимости и размеру. Привязка анимации, управляемой прокруткой, к элементам (кроме первого) выполняется в блоке @supports, чтобы браузеры без поддержки не получили объявление animation-timeline, которое они не смогут распарсить.

Эффект параллакса подписей

Применение отдельной временной шкалы к элементам <figcaption> создает параллаксное смещение, при котором подписи смещаются с другой скоростью, чем соответствующие им изображения. .carousel-item использует overflow: clip (вместо overflow: hidden), поэтому подпись не обрезается при движении по оси X (translateX).

@keyframes caption-parallax {
  from {
    transform: translateX(-30px);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

Небольшая задержка animation-range, начинающаяся с entry 20%, означает, что анимация подписи начинается после того, как изображение начало плавно появляться, что создает эффект постепенного раскрытия. animation-timeline и animation-range для figcaption применяются внутри защитника @supports (см. раздел, посвященный прогрессивному улучшению).

Привязка индикатора прогресса к позиции прокрутки

Псевдоэлемент ::before на контейнере, анимированный с помощью именованной временной шкалы прокрутки --carousel, создает индикатор выполнения, отслеживающий общее положение прокрутки.

.carousel::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 3px;
  width: calc(100% - 2rem);
  background: #0070f3;
  transform-origin: left;
  z-index: 20;
  pointer-events: none;
}

@keyframes progress-bar {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

Временная шкала --carousel ссылается на индикатор прогресса прокрутки по оси линии контейнера (определенной с помощью scroll-timeline: --carousel inline на .carousel). По мере прокрутки карусели, индикатор увеличивается от 0 до полной ширины. width: calc(100% - 2rem) учитывает левый и правый отступы контейнера, чтобы индикатор соответствовал видимой ширине. pointer-events: none предотвращает перехват индикатором кликов по элементам карусели. transform: scaleX() дружелюбно к компоновщику; анимация выполняется вне основного потока без затрат на компоновку или отрисовку. animation-timeline для ::before применяется внутри защитника @supports (см. раздел, посвященный прогрессивному улучшению).

Готовая карусель - полный код

Полный HTML

<div class="carousel" role="region" aria-label="Featured images">
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/1/800/300" alt="Mountain landscape at sunrise" />
    <figcaption>Mountain Sunrise</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/2/800/300" alt="Ocean waves crashing on rocks" />
    <figcaption>Coastal Waves</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/3/800/300" alt="Dense forest canopy from below" />
    <figcaption>Forest Canopy</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/4/800/300" alt="Desert dunes under a clear sky" />
    <figcaption>Desert Dunes</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/5/800/300" alt="Snow-covered mountain peak" />
    <figcaption>Alpine Peak</figcaption>
  </figure>
  <figure class="carousel-item">
    <img src="https://picsum.photos/seed/6/800/300" alt="Wildflower meadow in spring" />
    <figcaption>Spring Meadow</figcaption>
  </figure>
</div>

Полный CSS

/* ==============================================
   Базовый макет карусели и привязка прокрутки
   ============================================== */

.carousel {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 85%;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: auto;
  padding: 1rem;
  position: relative;
  box-sizing: border-box;
  scroll-timeline: --carousel inline;
}

.carousel::-webkit-scrollbar {
  display: none;
}

/* Индикатор прогресса — базовое позиционирование (анимация применяется в @supports) */
.carousel::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 3px;
  width: calc(100% - 2rem);
  background: #0070f3;
  transform-origin: left;
  transform: scaleX(0);
  z-index: 20;
  pointer-events: none;
}

.carousel-item {
  scroll-snap-align: center;
  border-radius: 0.75rem;
  overflow: clip;
  overflow-clip-margin: 0px;
  margin: 0;
}

/* Первый элемент находится в области просмотра при загрузке страницы; рендерим его в финальном состоянии */
.carousel-item:first-child {
  opacity: 1;
  transform: scale(1);
}

.carousel-item img {
  width: 100%;
  height: 300px;
  object-fit: cover;
  display: block;
}

.carousel-item figcaption {
  padding: 0.75rem 1rem;
  font-size: 1rem;
  background: #111;
  color: #fff;
}

/* ==============================================
   Анимация
   ============================================== */

@keyframes fade-scale-in {
  from { opacity: 0.3; transform: scale(0.9); }
  to { opacity: 1; transform: scale(1); }
}

@keyframes caption-parallax {
  from { transform: translateX(-30px); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

@keyframes progress-bar {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

/* ==============================================
   Кнопки прокрутки с защитой от неподдерживающих браузеров
   ============================================== */

@supports selector(::scroll-button(right)) {
  .carousel {
    scrollbar-width: none;
  }

  .carousel::-webkit-scrollbar {
    display: none;
  }

  .carousel::scroll-button(left),
  .carousel::scroll-button(right) {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;
    width: 3rem;
    height: 3rem;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    font-size: 1.25rem;
    display: grid;
    place-content: center;
    cursor: pointer;
    border: 2px solid rgba(255, 255, 255, 0.3);
    transition: background 0.2s ease, opacity 0.2s ease;
  }

  .carousel::scroll-button(left) {
    content: "◀";
    left: 0.5rem;
  }

  .carousel::scroll-button(right) {
    content: "▶";
    right: 0.5rem;
  }

  .carousel::scroll-button(left):hover,
  .carousel::scroll-button(right):hover {
    background: rgba(0, 0, 0, 0.85);
  }

  .carousel::scroll-button(left):disabled,
  .carousel::scroll-button(right):disabled {
    opacity: 0.3;
    cursor: default;
  }
}

/* ==============================================
   Маркеры прокрутки с защитой
   ============================================== */

@supports selector(::scroll-marker()) {
  .carousel::scroll-marker-group {
    display: flex;
    justify-content: center;
    gap: 0.5rem;
    padding-top: 1rem;
  }

  .carousel-item::scroll-marker() {
    content: "";
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.25);
    border: 2px solid transparent;
    transition: background 0.3s ease, transform 0.3s ease;
    cursor: pointer;
  }

  .carousel-item::scroll-marker():checked {
    background: #0070f3;
    transform: scale(1.3);
    border-color: #0070f3;
  }
}

/* ==============================================
   Анимация, управляемая прокруткой, с защитой
   ============================================== */

@supports (animation-timeline: view()) {
  .carousel-item:not(:first-child) {
    animation: fade-scale-in 1s linear both;
    animation-timeline: view(inline);
    animation-range: entry 0% entry 100%;
  }

  .carousel-item figcaption {
    animation: caption-parallax 1s linear both;
    animation-timeline: view(inline);
    animation-range: entry 20% entry 100%;
  }

  .carousel::before {
    animation: progress-bar 1s linear both;
    animation-timeline: --carousel;
  }
}

/* ==============================================
   Отключение анимации
   ============================================== */

@media (prefers-reduced-motion: reduce) {
  .carousel-item,
  .carousel-item figcaption,
  .carousel::before {
    animation: none;
    animation-timeline: none;
    animation-range: normal;
  }
}

Демо

Прогрессивное улучшение и резерв

Определение поддержки с помощью @supports

Оборачивание стилей новых псевдоэлементов в блоки @supports позволяет браузерам, которые их не поддерживают, просто рендерить прокручиваемый контейнер. В полном CSS выше ::scroll-button(), ::scroll-marker() и animation-timeline помещены в блоки @supports. Структура такая:

/* Кнопки прокрутки: рендерятся только в поддерживающих браузерах */
@supports selector(::scroll-button(right)) {
  /* Скрываем полосу прокрутки, стилизуем ::scroll-button(left) и ::scroll-button(right) */
}

/* Маркеры прокрутки: рендерятся только в поддерживающих браузерах */
@supports selector(::scroll-marker()) {
  /* Стилизуем ::scroll-marker-group и ::scroll-marker() с состоянием :checked */
}

/* Анимация, управляемая прокруткой: привязываем временные шкалы только в случае поддержки */
@supports (animation-timeline: view()) {
  /* Привязываем анимации fade-scale-in, caption-parallax и progress-bar */
}

Без этих возможностей пользователи получат горизонтально прокручиваемый контейнер с видимой полосой прокрутки. Главный пользовательский опыт остается неизменным.

Когда по-прежнему нужен JS

CSS-карусели не охватывают все варианты использования. Автовоспроизведение с паузой при наведении курсора требует таймеров, которые невозможно реализовать с помощью CSS. Бесконечный цикл требует манипуляций с DOM или клонированных узлов. Нам по-прежнему нужны скрипты для динамической загрузки контента (ленивая загрузка слайдов, получаемых из API). Сложные требования к доступности, такие как аннотации в реальном времени при смене активного слайда или пользовательское управление фокусом, выходящее за рамки того, что предоставляет браузер, по-прежнему требуют тонкого слоя JS. Правильный подход заключается в том, чтобы рассматривать эти CSS API как основу и добавлять скрипты только для тех действий, которые платформа еще не обрабатывает нативно.

Чеклист реализации

  • семантический HTML с role="region" и aria-label

  • scroll-snap-type: x mandatory на контейнере

  • scroll-snap-align на каждом элементе

  • scroll-behavior: smooth на контейнере для анимированной прокрутки

  • ::scroll-button(left) и ::scroll-button(right) со стилизованным контентом внутри @supports

  • ::scroll-marker() на элементах с активным состоянием через :checked внутри @supports

  • позиционирование и стилизация ::scroll-marker-group внутри @supports

  • резервы @supports для неподдерживающих браузеров (полоса прокрутки видна, когда недоступны кнопки)

  • тестирование навигации с помощью клавиатуры (Tab, стрелки)

  • отключение анимации через @media (prefers-reduced-motion: reduce)

  • тестирование в Chrome, Safari и Firefox

Значение для фронтенд-разработки

То, о чем мы говорили в статье, является частью более широкой траектории платформы. Вложенность CSS, :has(), запросы к контейнеру, а теперь и анимация, управляемая прокруткой, заменяют логику, реализация которой раньше требовала JS или сборщика. Улучшение производительности очевидно: 0 КБ JS в сборке для функционала карусели (в сравнении с 40-140 КБ библиотеки) и анимации, управляемые прокруткой, которые выполняются в потоке компоновщика с ускорением графического процессора, что позволяет избежать компоновки и отрисовки в основном потоке для свойств, доступных компоновщику, таких как transform и opacity.

Разработчикам, работающим со Swiper, Flickity или Embla, следует оценить, действительно ли их сценарии использования выходят за рамки того, что предоставляют современные CSS API. CSS сейчас обрабатывает навигацию с привязкой, точечные индикаторы, анимацию появления при прокрутке и отслеживание прогресса “из коробки”. Если вашей карусели не требуется автовоспроизведение, бесконечный цикл или динамическая загрузка слайдов, откажитесь от библиотеки. Начните внедрять эти API постепенно, оберните новые функции в @supports и откажитесь от зависимостей, которые эти спецификации были призваны заменить.

Для более подробного ознакомления с рассматриваемыми API можно обратиться к спецификации W3C CSS Overflow Level 5 и документации MDN по анимации, управляемой прокруткой, которые являются авторитетными источниками информации.