Реализация параллакс-эффектов при скролле на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация параллакс-эффектов при скролле на сайте
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация параллакс-эффектов при скролле на сайте

Параллакс — разная скорость движения слоёв при скролле, создающая иллюзию глубины. Реализуется тремя способами: CSS background-attachment: fixed (ограниченно), JavaScript с requestAnimationFrame (универсально), или GSAP ScrollTrigger scrub (максимально гладко). CSS-подход ломается на iOS Safari из-за того, как мобильный браузер оптимизирует рендеринг скролла — практически всегда нужен JS.

CSS параллакс (десктоп-only)

/* Только для фоновых изображений, не работает на iOS Safari */
.parallax-section {
  background-image: url('/hero-bg.jpg');
  background-attachment: fixed;
  background-size: cover;
  background-position: center;
  min-height: 100vh;
}

/* Альтернатива через transform с will-change */
.parallax-layer {
  will-change: transform;
  transform: translateZ(0); /* форсируем GPU-ускорение */
}

JavaScript: оптимизированный parallax

Ключевые правила производительности:

  1. Читать scrollY только из scroll события (или IntersectionObserver для определения видимости)
  2. Писать в DOM только через requestAnimationFrame
  3. Использовать transform вместо top/left — не вызывает reflow
  4. Добавлять will-change: transform перед началом анимации
// hooks/useParallax.ts
import { useEffect, useRef, useState } from 'react'

interface ParallaxOptions {
  speed?: number        // 0 = не двигается, 1 = вместе со скроллом, -1 = обратно
  direction?: 'vertical' | 'horizontal'
  disabled?: boolean    // для мобильных
}

export function useParallax({
  speed = 0.5,
  direction = 'vertical',
  disabled = false,
}: ParallaxOptions = {}) {
  const elementRef = useRef<HTMLElement>(null)
  const rafRef = useRef<number | null>(null)
  const lastScrollY = useRef(0)

  useEffect(() => {
    if (disabled || !elementRef.current) return

    const el = elementRef.current
    el.style.willChange = 'transform'

    let ticking = false

    const updateTransform = () => {
      const rect = el.getBoundingClientRect()
      const viewportCenter = window.innerHeight / 2
      const elementCenter = rect.top + rect.height / 2
      const distanceFromCenter = elementCenter - viewportCenter

      const offset = -distanceFromCenter * (speed - 1)

      if (direction === 'vertical') {
        el.style.transform = `translateY(${offset}px)`
      } else {
        el.style.transform = `translateX(${offset}px)`
      }

      ticking = false
    }

    const onScroll = () => {
      if (!ticking) {
        rafRef.current = requestAnimationFrame(updateTransform)
        ticking = true
      }
    }

    window.addEventListener('scroll', onScroll, { passive: true })
    updateTransform() // начальное положение

    return () => {
      window.removeEventListener('scroll', onScroll)
      if (rafRef.current) cancelAnimationFrame(rafRef.current)
      el.style.willChange = ''
      el.style.transform = ''
    }
  }, [speed, direction, disabled])

  return elementRef
}
// components/ParallaxImage.tsx
import { useParallax } from '../hooks/useParallax'

interface ParallaxImageProps {
  src: string
  alt: string
  speed?: number
  className?: string
}

export function ParallaxImage({ src, alt, speed = 0.6, className }: ParallaxImageProps) {
  // Отключаем параллакс на мобильных — экономим ресурсы
  const isMobile = typeof window !== 'undefined'
    ? window.matchMedia('(max-width: 768px)').matches
    : false

  const ref = useParallax({ speed, disabled: isMobile })

  return (
    <div className="overflow-hidden">
      <img
        ref={ref as React.RefObject<HTMLImageElement>}
        src={src}
        alt={alt}
        className={className}
        // Немного увеличиваем изображение чтобы скрыть края при параллаксе
        style={{ transform: 'scale(1.1)', transformOrigin: 'center' }}
      />
    </div>
  )
}

GSAP ScrollTrigger scrub (рекомендуется)

GSAP обрабатывает параллакс через scrub — привязывает анимацию к позиции скролла с опциональным сглаживанием:

// components/ParallaxSection.tsx
import { useEffect, useRef } from 'react'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'

gsap.registerPlugin(ScrollTrigger)

export function ParallaxSection() {
  const sectionRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const ctx = gsap.context(() => {
      // Слой 1: медленный фон
      gsap.to('.bg-layer', {
        yPercent: -20,
        ease: 'none',
        scrollTrigger: {
          trigger: sectionRef.current,
          start: 'top bottom',
          end: 'bottom top',
          scrub: true,
        },
      })

      // Слой 2: средний план
      gsap.to('.mid-layer', {
        yPercent: -40,
        ease: 'none',
        scrollTrigger: {
          trigger: sectionRef.current,
          start: 'top bottom',
          end: 'bottom top',
          scrub: 1.5, // задержка сглаживания в секундах
        },
      })

      // Слой 3: передний план (быстрее всех)
      gsap.to('.fg-layer', {
        yPercent: -60,
        ease: 'none',
        scrollTrigger: {
          trigger: sectionRef.current,
          start: 'top bottom',
          end: 'bottom top',
          scrub: 2,
        },
      })

      // Горизонтальный параллакс для декоративных элементов
      gsap.to('.float-left', {
        x: -50,
        ease: 'none',
        scrollTrigger: {
          trigger: sectionRef.current,
          start: 'top bottom',
          end: 'bottom top',
          scrub: true,
        },
      })

      gsap.to('.float-right', {
        x: 50,
        ease: 'none',
        scrollTrigger: {
          trigger: sectionRef.current,
          start: 'top bottom',
          end: 'bottom top',
          scrub: true,
        },
      })
    }, sectionRef)

    return () => ctx.revert()
  }, [])

  return (
    <div ref={sectionRef} className="relative h-screen overflow-hidden">
      <div className="bg-layer absolute inset-0 bg-cover bg-center"
        style={{ backgroundImage: "url('/bg-mountains.jpg')", height: '120%', top: '-10%' }}
      />
      <div className="mid-layer absolute inset-0 flex items-center justify-center">
        <h2 className="text-6xl font-bold text-white">Заголовок</h2>
      </div>
      <div className="fg-layer absolute bottom-0 w-full">
        <svg viewBox="0 0 1440 200">{/* облака, деревья */}</svg>
      </div>
    </div>
  )
}

Параллакс мышью (3D-наклон карточки)

// components/TiltCard.tsx
import { useRef, MouseEvent } from 'react'
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'

export function TiltCard({ children }: { children: React.ReactNode }) {
  const cardRef = useRef<HTMLDivElement>(null)
  const mouseX = useMotionValue(0)
  const mouseY = useMotionValue(0)

  const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [10, -10]), {
    stiffness: 200, damping: 20
  })
  const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-10, 10]), {
    stiffness: 200, damping: 20
  })

  const handleMouseMove = (e: MouseEvent) => {
    const rect = cardRef.current!.getBoundingClientRect()
    const x = (e.clientX - rect.left) / rect.width - 0.5
    const y = (e.clientY - rect.top) / rect.height - 0.5
    mouseX.set(x)
    mouseY.set(y)
  }

  const handleMouseLeave = () => {
    mouseX.set(0)
    mouseY.set(0)
  }

  return (
    <motion.div
      ref={cardRef}
      style={{ rotateX, rotateY, transformStyle: 'preserve-3d' }}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
      className="cursor-pointer"
    >
      {children}
    </motion.div>
  )
}

Respect prefers-reduced-motion

// hooks/useReducedMotion.ts
import { useEffect, useState } from 'react'

export function useReducedMotion(): boolean {
  const [reducedMotion, setReducedMotion] = useState(false)

  useEffect(() => {
    const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
    setReducedMotion(mq.matches)
    const handler = (e: MediaQueryListEvent) => setReducedMotion(e.matches)
    mq.addEventListener('change', handler)
    return () => mq.removeEventListener('change', handler)
  }, [])

  return reducedMotion
}

Типичные сроки

CSS параллакс для одного hero-раздела — 3–4 часа. JS/GSAP параллакс для 3–5 секций с несколькими слоями — 2–3 рабочих дня. Полная сцена с мышью, 3D-наклоном, мобильным fallback и тестами — 3–5 дней.