Слайды

Дорожка, слайдер, галерея с превью и лайтбокс — семейство прокрутки слоя GriffinJS. Один механизм с разной навигацией и обёрткой; без скрипта у каждого есть рабочая база.

GriffinJS packages/ui/scss/griffinjs/_track.scss, _gallery.scss, _lightbox.scss packages/ui/src/griffinjs/widgets/

Бейдж нужен griffinjs.js у примера означает, что показанное поведение даёт скрипт. У примеров без бейджа разметка обязана работать без него — это проверяется на полигоне с отключённым скриптом.

Подключение

<link rel="stylesheet" href="griffincss-ui.css">
<link rel="stylesheet" href="griffinjs.css">
<script src="griffinjs.js"></script>

griffinjs.css подключается поверх griffincss-ui.css: у него нет собственного блока :root, токены берутся из ядра. Скрипт поднимает виджеты по атрибутам data-gr-* сам; атрибут data-auto="false" на теге отключает автозапуск — тогда GriffinJS.start() зовётся вручную.

Дорожка

.gr-track — scroll-snap-контейнер. Каждый прямой потомок — слайд во всю ширину; свайп, инерция, колесо и RTL — от браузера. Это база без JS для всего семейства.

Класс / атрибутНазначение
.gr-trackДорожка: flex, scroll-snap-type: x mandatory, без полосы прокрутки
.gr-track-2 / -3 / -4Слайдов в ряд; суффикс брейкпоинта — .gr-track-1-sm .gr-track-3-md; с буквой c — по ширине ближайшего .gr-cq: .gr-track-3-cmd
.gr-track-scaleСоседи активного слайда чуть меньше — на --gr-progress; без скрипта — натуральная величина
.gr-track-stack-c{sm,md,lg,xl}Столбик, пока ближайший предок с .gr-cq уже порога (640 / 768 / 1024 / 1280 px): подборка в боковой колонке — список, в содержимом — лента. Оконных порогов нет: в узком окне лента остаётся лентой. Слайдеру не подходит — кнопки и точки останутся
data-gr-track нужен griffinjs.jsКлавиатура (←→ с учётом RTL, Home, End), aria-current и inert на слайдах, --gr-progress, событие griffin:change
<div class="gr-track gr-track-2-sm gr-track-3-md" tabindex="0" aria-label="Категории">
  <div class="gr-card gr-p-4">Первая</div>
  <div class="gr-card gr-p-4">Вторая</div>
  …
</div>
Дорожка без скрипта: три в ряд от md
Ноутбуки
Планшеты
Телефоны
Наушники
Часы
Аксессуары
<aside class="gr-cq-aside">
  <div class="gr-track gr-track-stack-csm gr-track-3-cmd" tabindex="0" aria-label="Похожие товары">
    <div class="gr-card gr-p-4">…</div>
    …
  </div>
</aside>
Одна разметка: в узком гнезде (14rem) — столбик, во всю ширину колонки — лента
Кресло «Дюна»
Торшер «Свет»
Полка «Норд»
Кресло «Дюна»
Торшер «Свет»
Полка «Норд»
Стол «Лофт»

Параметры data-gr-track

ПараметрПо умолчаниюСмысл
loopfalseБесконечная дорожка: индекс идёт по кругу
rewindfalseС края — на другой край без клонов
step1Шаг next()/prev(): число или page
index0Начальный слайд
keyboardtrueСтрелки, Home, End
inerttrueНевидимые слайды помечаются inert
labelСлайд {i} из {n}Подпись слайда без своей aria-label
enginescrollДвижок движения; fade — отдельным модулем

Слайдер

data-gr-slider на обёртке превращает дорожку в слайдер: кнопки [data-gr-prev] / [data-gr-next], точки в [data-gr-dots], автоплей с паузой. Органы управления спрятаны CSS, пока скрипт не поставил data-gr-state на корень — бездействующая кнопка хуже отсутствующей.

ПараметрПо умолчаниюСмысл
autoplay0Миллисекунд между слайдами; 0 — выключено. Уважает prefers-reduced-motion
loopfalseПо кругу; без цикла кнопки у краёв получают aria-disabled
rewindfalse«Бесконечно» без клонов: с последней страницы — на первую, с первой — на последнюю; кнопки у краёв не блокируются
step1Шаг кнопок, точек и свайпа: число слайдов или page — на столько, сколько видно. Снап-точки дорожка ставит только началам страниц (--gr-snap на слайдах), так что палец останавливается там же, куда ведут точки
pause-on-hovertrueНаведение и фокус держат автоплей; кнопка паузы фиксирует его
dotstrueСтроить точки в [data-gr-dots] — по страницам, не по слайдам

loop — бесшовный переход через край. У движка scroll это клоны краёв: по одному видимому слайду с каждой стороны, помеченные data-gr-clone, aria-hidden и inert; дорожка доезжает до клона плавно и на остановке мгновенно перескакивает на настоящий слайд. У fade цикл бесплатен. Если движок цикла не умеет, loop ведёт себя как rewind.

Шаг и rewind

Пять карточек по две в ряд. Первый слайдер листает step: 2 — ровно на две карточки, точек три (последняя страница — одна карточка, выровненная к концу); второй — step: page, то же самое, но шаг подстраивается под число видимых на любой ширине. У обоих rewind: с последней страницы кнопка «вперёд» возвращает к первой, и у краёв ничего не блокируется. Сравните с третьим — без rewind кнопка у края гаснет.

<div class="gr-slider" data-gr-slider="step: 2; rewind">    <!-- на две карточки -->
<div class="gr-slider" data-gr-slider="step: page; rewind"> <!-- на сколько видно -->
<div class="gr-slider" data-gr-slider="step: page">         <!-- без rewind: у края кнопка недоступна -->
step: 2; rewind нужен griffinjs.js
1
2
3
4
5
step: page; rewind нужен griffinjs.js
1
2
3
4
5
step: page без rewind нужен griffinjs.js
1
2
3
4
5

Когда в ряд видно несколько слайдов, крайний достижимый индекс — count − perView: последний слайд выравнивается к концу дорожки, и дальше браузер не прокрутит. Точки строятся по страницам от этого предела (при step: page последняя страница может быть короче шага), а при смене брейкпоинта — перестраиваются: дорожка шлёт griffin:layout.

ЧастьЧто делает скрипт
кореньrole="region", aria-roledescription="carousel", data-gr-state: ready / playing / paused
.gr-trackaria-live="polite"; при автоплее — off
[data-gr-dots]кнопки .gr-slider-dot по одной на страницу, активная — aria-current
[data-gr-pause]кнопка паузы; создаётся сама, если автоплей включён, а кнопки нет (WCAG 2.2.2). Ход — кольцом из --gr-progress
<div class="gr-slider" data-gr-slider="autoplay: 4000; loop" aria-label="Акции">
  <div class="gr-track">
    <div class="frame frame-1">Первый</div>
    <div class="frame frame-2">Второй</div>
    <div class="frame frame-3">Третий</div>
  </div>
  <button class="gr-btn gr-btn-icon gr-slider-prev" data-gr-prev aria-label="Назад">‹</button>
  <button class="gr-btn gr-btn-icon gr-slider-next" data-gr-next aria-label="Вперёд">›</button>
  <div class="gr-slider-dots" data-gr-dots></div>
</div>
Слайдер с автоплеем и циклом нужен griffinjs.js
Первый
Второй
Третий

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

Событие и API

Каждое изменение слайда — событие griffin:change на дорожке, всплывающее до документа: detail.index (виртуальный индекс, в цикле растёт бесконечно), detail.physical (позиция в DOM), detail.previous.

const el = document.querySelector('.gr-slider');
const slider = GriffinJS.instance(el, 'slider');

slider.next();          // slider.prev(), slider.goTo(2), slider.pause(), slider.play()
slider.track.index;     // текущий индекс

el.addEventListener('griffin:change', (e) => {
  console.log(e.detail.physical, e.detail.previous);
});

// Дорожка вручную, без атрибута:
const track = GriffinJS.track(document.querySelector('.gr-track'), { loop: true });

Движок fade

Слайды стопкой, переход прозрачностью. Разметка та же, плюс класс .gr-track-fade: CSS кладёт слайды в одну ячейку сетки и связывает opacity с --gr-progress. Движок ничего не двигает — он публикует позицию, переход делает CSS-transition. Свайп и тяга — через примитив gesture: пока палец на экране, прозрачность следует за ним без сглаживания (data-gr-state="dragging" на дорожке). Цикл — бесплатно, без клонов. Без скрипта показан первый слайд.

<div class="gr-slider" data-gr-slider="engine: fade; loop; autoplay: 4000">
  <div class="gr-track gr-track-fade">
    <div class="frame frame-1">Первый</div>
    …
  </div>
  <div class="gr-slider-dots" data-gr-dots></div>
</div>
Fade: потяните пальцем или мышью нужен griffinjs.js
Первый
Второй
Третий

Галерея

data-gr-gallery — слайдер плюс полоса превью .gr-gallery-thumbs: щелчок по превью ведёт большую дорожку, активное превью получает aria-current и подъезжает в видимую часть полосы. Превью — кнопки: клавиатура и фокус у них свои. Параметры — те же, что у слайдера.

<div class="gr-gallery" data-gr-gallery aria-label="Фотографии товара">
  <div class="gr-track">
    <div class="frame frame-1 frame-square">1</div>
    …
  </div>
  <div class="gr-track gr-gallery-thumbs">
    <button type="button"><div class="frame frame-1 frame-square"></div></button>
    …
  </div>
</div>
Галерея с превью нужен griffinjs.js

Лайтбокс

data-gr-lightbox на ссылке или кнопке. Значение — имя группы: элементы с одним значением листаются в одном окне; пустое значение — окно на один кадр. Окно — <dialog class="gr-modal gr-lightbox">, строится при открытии и уничтожается при закрытии, поэтому видео и iframe останавливаются сами. База без JS — обычная ссылка на большое изображение.

АтрибутСмысл
data-gr-lightbox="группа"Что открывать; адрес — из href или data-gr-src
data-gr-captionПодпись под кадром
data-gr-altАльтернативный текст; по умолчанию — alt вложенной картинки
data-gr-typeimage / video / iframe; по умолчанию угадывается по адресу (mp4/webm, YouTube, Vimeo)
<a href="data:image/svg+xml,…" data-gr-lightbox="promo" data-gr-caption="Синий">
  <div class="frame frame-1 frame-square"></div>
</a>
<a href="data:image/svg+xml,…" data-gr-lightbox="promo" data-gr-caption="Зелёный">…</a>

<!-- из скрипта -->
GriffinJS.lightbox.open([{ src: 'a.jpg', caption: 'Первая' }, { src: 'b.mp4' }], 0);
Группа из трёх кадров нужен griffinjs.js
1
2
3

Прогресс движения

Дорожка публикует --gr-progress: на каждом слайде — 1 на своём месте и 0 на расстоянии в один слайд; на самой дорожке — долю пройденного пути. Скрипт не пишет ни transform, ни opacity: эффект целиком в CSS, и запасное значение делает его безопасным без скрипта.

/* соседи меньше активного — готовый класс .gr-track-scale делает то же */
.my-track > * { scale: calc(0.9 + 0.1 * var(--gr-progress, 1)); }

/* полоса прогресса из --gr-progress дорожки */
.my-bar { inline-size: calc(var(--gr-progress, 0) * 100%); }

Примеры использования

Рецепты ниже собирают семейство прокрутки с компонентами и утилитами библиотеки: баннер витрины, товары в ряд, отзывы, галерея товара, лайтбокс поверх сетки карточек и лента без единой строки скрипта.

Баннер витрины: автоплей, цикл, точки

Кадр во всю ширину, кнопки по краям, точки под кадром. Автоплей останавливается наведением и фокусом, а кнопка паузы в углу показывает ход кольцом — WCAG 2.2.2 закрыт без единой своей строки. Слайды — обычные блоки: внутрь идут .gr-card, кнопки, любая раскладка.

<div class="gr-slider gr-radius-3 gr-overflow-hidden" data-gr-slider="autoplay: 5000; loop" aria-label="Баннеры">
  <div class="gr-track">
    <div class="frame frame-1 gr-p-6 gr-text-start">
      <div><h3 class="gr-m-0">Осенняя коллекция</h3><a class="gr-btn gr-mt-3" href="#">Смотреть</a></div>
    </div>
    …
  </div>
  <button class="gr-btn gr-btn-icon gr-slider-prev" data-gr-prev aria-label="Назад">‹</button>
  <button class="gr-btn gr-btn-icon gr-slider-next" data-gr-next aria-label="Вперёд">›</button>
  <div class="gr-slider-dots" data-gr-dots></div>
</div>
Баннер нужен griffinjs.js

Осенняя коллекция

Смотреть

Скидки на наушники

К товарам

Доставка за день

Условия

Товары в ряд: один на телефоне, четыре на десктопе

Слайдер и адаптивная дорожка — одна разметка: .gr-track-2-sm .gr-track-4-lg задаёт число карточек в ряд, step: page листает сразу на страницу, rewind с последней возвращает к первой. Точек столько, сколько страниц на текущей ширине — на телефоне шесть, на десктопе две. Карточки — .gr-card из компонентов, ничего специального.

<div class="gr-slider" data-gr-slider="step: page; rewind" aria-label="Хиты продаж">
  <div class="gr-track gr-track-2-sm gr-track-4-lg">
    <div class="gr-card gr-p-4">…</div>
    ×6
  </div>
  <button class="gr-btn gr-btn-icon gr-slider-prev" data-gr-prev aria-label="Назад">‹</button>
  <button class="gr-btn gr-btn-icon gr-slider-next" data-gr-next aria-label="Вперёд">›</button>
  <div class="gr-slider-dots" data-gr-dots></div>
</div>
Карточки в ряд: постранично, по кругу без клонов нужен griffinjs.js
Наушники

4 990 ₽

Колонка

7 490 ₽

Часы

12 990 ₽

Планшет

24 990 ₽

Клавиатура

3 490 ₽

Мышь

1 990 ₽

Отзывы: соседи чуть меньше активного

.gr-track-scale уменьшает соседей на величину, которую дорожка пишет в --gr-progress. Эффект следует за пальцем при свайпе — без единого кадра анимации в скрипте. При prefers-reduced-motion масштаб отключён. Листается по одному отзыву с rewind: при трёх в ряд из четырёх точек две — по достижимым страницам.

<div class="gr-slider" data-gr-slider="rewind" aria-label="Отзывы">
  <div class="gr-track gr-track-scale gr-track-3-md gr-py-4">
    <blockquote class="gr-card gr-p-4 gr-m-0">…</blockquote>
    …
  </div>
  <div class="gr-slider-dots" data-gr-dots></div>
</div>
Масштаб соседей из --gr-progress нужен griffinjs.js

«Собрал витрину за вечер — раскладка строкой, слайдер атрибутом».

— Анна, магазин керамики

«Без скрипта дорожка всё равно листается — это подкупило».

— Михаил, студия

«Тёмная тема и режим для слабовидящих — из коробки, в слайдере тоже».

— Ольга, агентство

«Лайтбокс без своей разметки: ссылка и атрибут».

— Сергей, фотограф

Галерея товара: превью снизу, лайтбокс по щелчку

Два виджета в одной разметке: галерея листает кадры и подсвечивает превью, а data-gr-lightbox на кадрах открывает их во весь экран той же группой. Оба берут разметку как есть; без скрипта остаются две дорожки и ссылки на большие изображения.

<div class="gr-gallery" data-gr-gallery aria-label="Фотографии">
  <div class="gr-track">
    <a href="large-1.jpg" data-gr-lightbox="product" data-gr-caption="Вид спереди"><img …></a>
    …
  </div>
  <div class="gr-track gr-gallery-thumbs">
    <button type="button"><img …></button> …
  </div>
</div>
Галерея + лайтбокс нужен griffinjs.js

Лента без скрипта

Не каждой дорожке нужен слайдер. Лента историй, ряд логотипов, полоса тегов — .gr-track с шириной слайдов через утилиты и ничего больше. Это же — «база без JS» любого слайдера на странице, где griffinjs.js не подключён.

<div class="gr-track gr-gap-sm" tabindex="0" aria-label="Истории">
  <a class="gr-w-24 gr-flex-none" href="#">…</a>
  …
</div>