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

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, 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 паралакс (тільки десктоп)

/* Тільки для фонових зображень, не працює на 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: оптимізований паралакс

Ключові правила продуктивності:

  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>
  )
}

Поважайте 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 днів.