Реализация анимаций при появлении элементов в области видимости (Scroll Reveal)

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

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация анимаций при появлении элементов в области видимости (Scroll Reveal)
Простая
от 1 рабочего дня до 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

Реализация анимаций при появлении элементов в области видимости (Scroll Reveal)

Scroll reveal — анимации, которые запускаются когда элемент попадает в видимую область экрана. Базовый инструмент для этого — IntersectionObserver API. Он не требует scroll-событий, не блокирует главный поток, работает асинхронно. Библиотека AOS или ScrollReveal.js — это обёртки над тем же принципом, но с готовыми пресетами.

IntersectionObserver: минималистичная реализация

// hooks/useScrollReveal.ts
import { useEffect, useRef } from 'react'

interface ScrollRevealOptions {
  threshold?: number        // 0–1, доля видимости для срабатывания
  rootMargin?: string       // отступы (как у CSS margin, но для viewport)
  once?: boolean            // анимировать только первый раз
  delay?: number            // задержка в мс
}

export function useScrollReveal({
  threshold = 0.15,
  rootMargin = '0px 0px -50px 0px',
  once = true,
  delay = 0,
}: ScrollRevealOptions = {}) {
  const elementRef = useRef<HTMLElement>(null)

  useEffect(() => {
    const el = elementRef.current
    if (!el) return

    if (delay > 0) {
      el.style.transitionDelay = `${delay}ms`
    }

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          el.classList.add('is-visible')
          if (once) observer.unobserve(el)
        } else if (!once) {
          el.classList.remove('is-visible')
        }
      },
      { threshold, rootMargin }
    )

    observer.observe(el)
    return () => observer.disconnect()
  }, [threshold, rootMargin, once, delay])

  return elementRef
}
/* styles/scroll-reveal.css */

/* Базовое состояние — элемент скрыт */
.reveal {
  opacity: 0;
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

.reveal-fade-up {
  transform: translateY(40px);
}

.reveal-fade-down {
  transform: translateY(-40px);
}

.reveal-fade-left {
  transform: translateX(40px);
}

.reveal-fade-right {
  transform: translateX(-40px);
}

.reveal-scale {
  transform: scale(0.92);
}

/* Видимое состояние */
.reveal.is-visible {
  opacity: 1;
  transform: translate(0) scale(1);
}

/* Быстрое снятие анимации для prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

React-компонент Reveal

// components/Reveal.tsx
import { ReactNode, useRef, useEffect, useState } from 'react'

type RevealVariant = 'fade-up' | 'fade-down' | 'fade-left' | 'fade-right' | 'scale' | 'fade'

interface RevealProps {
  children: ReactNode
  variant?: RevealVariant
  delay?: number          // мс
  duration?: number       // мс
  threshold?: number
  once?: boolean
  className?: string
  as?: keyof JSX.IntrinsicElements
}

export function Reveal({
  children,
  variant = 'fade-up',
  delay = 0,
  duration = 600,
  threshold = 0.15,
  once = true,
  className = '',
  as: Tag = 'div',
}: RevealProps) {
  const ref = useRef<HTMLElement>(null)
  const [isVisible, setIsVisible] = useState(false)

  useEffect(() => {
    const el = ref.current
    if (!el) return

    // Уважаем настройки пользователя
    const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
    if (reducedMotion) {
      setIsVisible(true)
      return
    }

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true)
          if (once) observer.unobserve(el)
        } else if (!once) {
          setIsVisible(false)
        }
      },
      { threshold, rootMargin: '0px 0px -60px 0px' }
    )

    observer.observe(el)
    return () => observer.disconnect()
  }, [threshold, once])

  const baseStyle: React.CSSProperties = {
    transitionDuration: `${duration}ms`,
    transitionDelay: isVisible ? `${delay}ms` : '0ms',
    transitionProperty: 'opacity, transform',
    transitionTimingFunction: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
  }

  const hiddenStyles: Record<RevealVariant, React.CSSProperties> = {
    'fade-up':    { opacity: 0, transform: 'translateY(40px)' },
    'fade-down':  { opacity: 0, transform: 'translateY(-40px)' },
    'fade-left':  { opacity: 0, transform: 'translateX(40px)' },
    'fade-right': { opacity: 0, transform: 'translateX(-40px)' },
    'scale':      { opacity: 0, transform: 'scale(0.9)' },
    'fade':       { opacity: 0, transform: 'none' },
  }

  const style: React.CSSProperties = {
    ...baseStyle,
    ...(isVisible ? {} : hiddenStyles[variant]),
  }

  return (
    <Tag ref={ref as any} style={style} className={className}>
      {children}
    </Tag>
  )
}

Использование:

<Reveal variant="fade-up" delay={0}>
  <h2>Заголовок</h2>
</Reveal>

<Reveal variant="fade-up" delay={150}>
  <p>Текст с задержкой</p>
</Reveal>

<Reveal variant="scale" delay={300}>
  <button>Кнопка</button>
</Reveal>

Групповой stagger без задержки в JSX

Когда элементов много, передавать delay вручную неудобно. Группа-обёртка решает это:

// components/RevealGroup.tsx
import { Children, cloneElement, ReactElement } from 'react'
import { Reveal } from './Reveal'

interface RevealGroupProps {
  children: ReactNode
  staggerMs?: number
  variant?: RevealVariant
}

export function RevealGroup({
  children,
  staggerMs = 100,
  variant = 'fade-up',
}: RevealGroupProps) {
  return (
    <>
      {Children.map(children, (child, i) =>
        cloneElement(child as ReactElement, {
          delay: i * staggerMs,
          variant,
        })
      )}
    </>
  )
}
<RevealGroup staggerMs={80} variant="fade-up">
  <Reveal><div className="card">1</div></Reveal>
  <Reveal><div className="card">2</div></Reveal>
  <Reveal><div className="card">3</div></Reveal>
</RevealGroup>

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

Готовый хук + CSS-классы для 3–4 вариантов анимации — 3–4 часа. Компонент Reveal с группами, stagger, поддержкой prefers-reduced-motion и TypeScript — 1 рабочий день.