← HTML & CSS/Scroll Snap и Scroll-driven Animations#42 из 383← ПредыдущийСледующий →+15 XP
Полезно по теме:Гайд: старт в frontendПрактика: DOM и событияТермин: DOMМаршрут: старт с нуля

Scroll Snap и Scroll-driven Animations

Прокрутка с фиксацией на элементах (как в каруселях или страницах-лендингах) и анимации, привязанные к скроллу — два современных CSS-инструмента для создания насыщенного UX без JavaScript.

Scroll Snap — фиксация при прокрутке

/* Контейнер: включаем snap */
.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;  /* Ось и строгость */
  scroll-padding: 16px;           /* Отступ от края до точки привязки */
  gap: 12px;
}

/* Элементы: задаём точку привязки */
.slide {
  flex-shrink: 0;
  width: 300px;
  scroll-snap-align: start;    /* start | center | end */
  scroll-snap-stop: always;    /* Запрет перепрыгивать несколько элементов */
}

scroll-snap-type

scroll-snap-type: x mandatory;     /* Горизонталь, обязательная привязка */
scroll-snap-type: y mandatory;     /* Вертикаль, обязательная */
scroll-snap-type: y proximity;     /* Привязка только когда близко */
scroll-snap-type: both mandatory;  /* Обе оси */

mandatory — всегда останавливается на snap-точке. Применяется для чётких переходов между секциями.

proximity — snap только если близко к точке. Мягче, подходит для длинных страниц.

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

html {
  scroll-snap-type: y mandatory;
}

section {
  height: 100vh;
  scroll-snap-align: start;
}

Scroll-driven Animations

CSS scroll-driven animations (2023) позволяют привязать анимацию к прокрутке:

/* Прогресс-бар чтения */
@keyframes progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: #7b2ff7;
  transform-origin: left;
  animation: progress linear;
  animation-timeline: scroll();       /* Привязываем к скроллу */
  animation-range: 0% 100%;           /* Начало и конец */
}

ScrollTimeline API (JavaScript)

const progress = document.querySelector('.progress')

const animation = progress.animate(
  [
    { transform: 'scaleX(0)' },
    { transform: 'scaleX(1)' },
  ],
  {
    timeline: new ScrollTimeline({
      source: document.documentElement,
      axis: 'block',
    }),
    fill: 'both',
  }
)

View Timeline — анимация при появлении в viewport

@keyframes fade-in {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

.reveal-on-scroll {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 40%;
  /* Анимация проигрывается когда элемент входит в viewport */
}

programmatic scroll-snap — scrollIntoView

// Плавная прокрутка к элементу
element.scrollIntoView({
  behavior: 'smooth',
  block: 'start',     // 'start' | 'center' | 'end' | 'nearest'
  inline: 'nearest',
})

// Программный скролл контейнера
container.scrollTo({
  left: targetX,
  behavior: 'smooth',
})

Примеры

Симуляция scroll snap: вычисление ближайшей точки привязки и плавная анимация к ней

// Scroll Snap — горизонтальная карусель с CSS и JS-управлением
const style = document.createElement('style')
style.textContent = `
  * { box-sizing: border-box; }
  body { font-family: sans-serif; padding: 16px; }
  .carousel-wrap { margin-bottom: 16px; }
  .carousel {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    gap: 12px;
    padding: 8px;
    border: 2px solid #e2e8f0;
    border-radius: 8px;
    scrollbar-width: none;
  }
  .carousel::-webkit-scrollbar { display: none; }
  .slide {
    flex-shrink: 0;
    width: 200px;
    height: 120px;
    border-radius: 8px;
    scroll-snap-align: start;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    font-weight: 700;
    color: white;
  }
  .controls { display: flex; gap: 8px; margin-top: 8px; }
  .dot {
    width: 10px; height: 10px;
    border-radius: 50%;
    background: #e2e8f0;
    cursor: pointer;
    transition: background 0.2s;
  }
  .dot.active { background: #7b2ff7; }
`
document.head.appendChild(style)

const wrap = document.createElement('div')
wrap.className = 'carousel-wrap'
document.body.appendChild(wrap)

const carousel = document.createElement('div')
carousel.className = 'carousel'
wrap.appendChild(carousel)

const colors = ['#7b2ff7', '#06b6d4', '#f59e0b', '#ef4444', '#10b981']
const slides = colors.map((bg, i) => {
  const slide = document.createElement('div')
  slide.className = 'slide'
  slide.style.background = bg
  slide.textContent = `Слайд ${i + 1}`
  carousel.appendChild(slide)
  return slide
})

// Dot-навигация
const dotsContainer = document.createElement('div')
dotsContainer.className = 'controls'
wrap.appendChild(dotsContainer)

const dots = slides.map((_, i) => {
  const dot = document.createElement('div')
  dot.className = 'dot' + (i === 0 ? ' active' : '')
  dot.addEventListener('click', () => {
    carousel.scrollTo({ left: slides[i].offsetLeft - carousel.offsetLeft - 8, behavior: 'smooth' })
  })
  dotsContainer.appendChild(dot)
  return dot
})

// Определяем активный слайд по scroll
carousel.addEventListener('scroll', () => {
  const scrollLeft = carousel.scrollLeft
  const slideWidth = slides[0].offsetWidth + 12  // ширина + gap
  const activeIndex = Math.round(scrollLeft / slideWidth)
  dots.forEach((dot, i) => dot.classList.toggle('active', i === activeIndex))
  console.log('Активный слайд:', activeIndex + 1)
})

// Программный переход
const nextBtn = document.createElement('button')
nextBtn.textContent = '→ Следующий'
nextBtn.style.cssText = 'padding: 6px 12px; background: #7b2ff7; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px;'
let currentSlide = 0
nextBtn.addEventListener('click', () => {
  currentSlide = (currentSlide + 1) % slides.length
  slides[currentSlide].scrollIntoView({ behavior: 'smooth', inline: 'start', block: 'nearest' })
})
document.body.appendChild(nextBtn)

console.log('Карусель с CSS scroll-snap создана!')
console.log('Количество слайдов:', slides.length)

Scroll Snap и Scroll-driven Animations

Прокрутка с фиксацией на элементах (как в каруселях или страницах-лендингах) и анимации, привязанные к скроллу — два современных CSS-инструмента для создания насыщенного UX без JavaScript.

Scroll Snap — фиксация при прокрутке

/* Контейнер: включаем snap */
.carousel {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;  /* Ось и строгость */
  scroll-padding: 16px;           /* Отступ от края до точки привязки */
  gap: 12px;
}

/* Элементы: задаём точку привязки */
.slide {
  flex-shrink: 0;
  width: 300px;
  scroll-snap-align: start;    /* start | center | end */
  scroll-snap-stop: always;    /* Запрет перепрыгивать несколько элементов */
}

scroll-snap-type

scroll-snap-type: x mandatory;     /* Горизонталь, обязательная привязка */
scroll-snap-type: y mandatory;     /* Вертикаль, обязательная */
scroll-snap-type: y proximity;     /* Привязка только когда близко */
scroll-snap-type: both mandatory;  /* Обе оси */

mandatory — всегда останавливается на snap-точке. Применяется для чётких переходов между секциями.

proximity — snap только если близко к точке. Мягче, подходит для длинных страниц.

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

html {
  scroll-snap-type: y mandatory;
}

section {
  height: 100vh;
  scroll-snap-align: start;
}

Scroll-driven Animations

CSS scroll-driven animations (2023) позволяют привязать анимацию к прокрутке:

/* Прогресс-бар чтения */
@keyframes progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: #7b2ff7;
  transform-origin: left;
  animation: progress linear;
  animation-timeline: scroll();       /* Привязываем к скроллу */
  animation-range: 0% 100%;           /* Начало и конец */
}

ScrollTimeline API (JavaScript)

const progress = document.querySelector('.progress')

const animation = progress.animate(
  [
    { transform: 'scaleX(0)' },
    { transform: 'scaleX(1)' },
  ],
  {
    timeline: new ScrollTimeline({
      source: document.documentElement,
      axis: 'block',
    }),
    fill: 'both',
  }
)

View Timeline — анимация при появлении в viewport

@keyframes fade-in {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

.reveal-on-scroll {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 40%;
  /* Анимация проигрывается когда элемент входит в viewport */
}

programmatic scroll-snap — scrollIntoView

// Плавная прокрутка к элементу
element.scrollIntoView({
  behavior: 'smooth',
  block: 'start',     // 'start' | 'center' | 'end' | 'nearest'
  inline: 'nearest',
})

// Программный скролл контейнера
container.scrollTo({
  left: targetX,
  behavior: 'smooth',
})

Примеры

Симуляция scroll snap: вычисление ближайшей точки привязки и плавная анимация к ней

// Scroll Snap — горизонтальная карусель с CSS и JS-управлением
const style = document.createElement('style')
style.textContent = `
  * { box-sizing: border-box; }
  body { font-family: sans-serif; padding: 16px; }
  .carousel-wrap { margin-bottom: 16px; }
  .carousel {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    gap: 12px;
    padding: 8px;
    border: 2px solid #e2e8f0;
    border-radius: 8px;
    scrollbar-width: none;
  }
  .carousel::-webkit-scrollbar { display: none; }
  .slide {
    flex-shrink: 0;
    width: 200px;
    height: 120px;
    border-radius: 8px;
    scroll-snap-align: start;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    font-weight: 700;
    color: white;
  }
  .controls { display: flex; gap: 8px; margin-top: 8px; }
  .dot {
    width: 10px; height: 10px;
    border-radius: 50%;
    background: #e2e8f0;
    cursor: pointer;
    transition: background 0.2s;
  }
  .dot.active { background: #7b2ff7; }
`
document.head.appendChild(style)

const wrap = document.createElement('div')
wrap.className = 'carousel-wrap'
document.body.appendChild(wrap)

const carousel = document.createElement('div')
carousel.className = 'carousel'
wrap.appendChild(carousel)

const colors = ['#7b2ff7', '#06b6d4', '#f59e0b', '#ef4444', '#10b981']
const slides = colors.map((bg, i) => {
  const slide = document.createElement('div')
  slide.className = 'slide'
  slide.style.background = bg
  slide.textContent = `Слайд ${i + 1}`
  carousel.appendChild(slide)
  return slide
})

// Dot-навигация
const dotsContainer = document.createElement('div')
dotsContainer.className = 'controls'
wrap.appendChild(dotsContainer)

const dots = slides.map((_, i) => {
  const dot = document.createElement('div')
  dot.className = 'dot' + (i === 0 ? ' active' : '')
  dot.addEventListener('click', () => {
    carousel.scrollTo({ left: slides[i].offsetLeft - carousel.offsetLeft - 8, behavior: 'smooth' })
  })
  dotsContainer.appendChild(dot)
  return dot
})

// Определяем активный слайд по scroll
carousel.addEventListener('scroll', () => {
  const scrollLeft = carousel.scrollLeft
  const slideWidth = slides[0].offsetWidth + 12  // ширина + gap
  const activeIndex = Math.round(scrollLeft / slideWidth)
  dots.forEach((dot, i) => dot.classList.toggle('active', i === activeIndex))
  console.log('Активный слайд:', activeIndex + 1)
})

// Программный переход
const nextBtn = document.createElement('button')
nextBtn.textContent = '→ Следующий'
nextBtn.style.cssText = 'padding: 6px 12px; background: #7b2ff7; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px;'
let currentSlide = 0
nextBtn.addEventListener('click', () => {
  currentSlide = (currentSlide + 1) % slides.length
  slides[currentSlide].scrollIntoView({ behavior: 'smooth', inline: 'start', block: 'nearest' })
})
document.body.appendChild(nextBtn)

console.log('Карусель с CSS scroll-snap создана!')
console.log('Количество слайдов:', slides.length)

Задание

Создай горизонтальную карусель с CSS Scroll Snap. Карусель должна содержать 5 слайдов, каждый шириной 280px. Добавь `scroll-snap-type: x mandatory` на контейнер и `scroll-snap-align: start` на каждый слайд. Скрой полосу прокрутки через `scrollbar-width: none`.

Подсказка

`scroll-snap-type: x mandatory` — горизонтальный snap с обязательной привязкой. `scroll-snap-align: start` — привязка к левому краю слайда. `scrollbar-width: none` — скрывает полосу прокрутки в Firefox. `flex-shrink: 0` — слайды не сжимаются.

Загружаем среду выполнения...
Загружаем AI-помощника...