Реализация анимации текста (Typed.js, Split Text) на сайте

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

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация анимации текста (Typed.js, Split Text) на сайте
Средняя
от 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

Реализация анимации текста (Typed.js, Split Text) на сайте

Текстовые анимации делятся на два класса: эффект печатающей машинки (Typed.js, TypeIt) и посимвольные/пословные анимации через разбивку текста (GSAP SplitText, anime.js, или ручная реализация). Первый класс создаёт иллюзию ввода в реальном времени. Второй позволяет animировать каждую букву или слово независимо — wave-эффекты, stagger, 3D-трансформации.

Typed.js: эффект печатающей машинки

npm install typed.js
// components/TypedText.tsx
import { useEffect, useRef } from 'react'
import Typed from 'typed.js'

interface TypedTextProps {
  strings: string[]
  typeSpeed?: number       // мс на символ
  backSpeed?: number       // мс на удаление символа
  backDelay?: number       // задержка перед удалением
  loop?: boolean
  showCursor?: boolean
  cursorChar?: string
  onComplete?: () => void
}

export function TypedText({
  strings,
  typeSpeed = 60,
  backSpeed = 30,
  backDelay = 1500,
  loop = true,
  showCursor = true,
  cursorChar = '|',
  onComplete,
}: TypedTextProps) {
  const elementRef = useRef<HTMLSpanElement>(null)
  const typedRef = useRef<Typed | null>(null)

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

    typedRef.current = new Typed(elementRef.current, {
      strings,
      typeSpeed,
      backSpeed,
      backDelay,
      loop,
      showCursor,
      cursorChar,
      onComplete: (self) => {
        onComplete?.()
      },
      // HTML-теги в строках будут применены как разметка
      contentType: 'html',
    })

    return () => {
      typedRef.current?.destroy()
    }
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <span>
      <span ref={elementRef} />
    </span>
  )
}
// Использование
<h1 className="text-5xl font-bold">
  Мы создаём{' '}
  <TypedText
    strings={[
      '<span class="text-blue-500">сайты</span>',
      '<span class="text-purple-500">приложения</span>',
      '<span class="text-pink-500">продукты</span>',
    ]}
    typeSpeed={70}
    backSpeed={40}
    loop
  />
</h1>

Кастомная реализация без библиотеки

Для простых случаев — своя реализация типпера без зависимостей:

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

interface TypewriterOptions {
  strings: string[]
  typeSpeed?: number
  deleteSpeed?: number
  pauseMs?: number
  loop?: boolean
}

export function useTypewriter({
  strings,
  typeSpeed = 60,
  deleteSpeed = 30,
  pauseMs = 2000,
  loop = true,
}: TypewriterOptions) {
  const [displayText, setDisplayText] = useState('')
  const [isTyping, setIsTyping] = useState(true)
  const indexRef = useRef(0)
  const charRef = useRef(0)
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

  useEffect(() => {
    const tick = () => {
      const current = strings[indexRef.current]

      if (isTyping) {
        if (charRef.current < current.length) {
          setDisplayText(current.slice(0, charRef.current + 1))
          charRef.current++
          timerRef.current = setTimeout(tick, typeSpeed)
        } else {
          // Пауза перед удалением
          timerRef.current = setTimeout(() => {
            setIsTyping(false)
            tick()
          }, pauseMs)
        }
      } else {
        if (charRef.current > 0) {
          setDisplayText(current.slice(0, charRef.current - 1))
          charRef.current--
          timerRef.current = setTimeout(tick, deleteSpeed)
        } else {
          // Переход к следующей строке
          indexRef.current = loop
            ? (indexRef.current + 1) % strings.length
            : Math.min(indexRef.current + 1, strings.length - 1)
          setIsTyping(true)
          timerRef.current = setTimeout(tick, typeSpeed)
        }
      }
    }

    timerRef.current = setTimeout(tick, typeSpeed)
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current)
    }
  }, [strings, typeSpeed, deleteSpeed, pauseMs, loop, isTyping])

  return { displayText, isTyping }
}

GSAP SplitText: посимвольные анимации

SplitText (платный плагин GSAP) разбивает текст на <div> по символам, словам или строкам:

// components/SplitTextReveal.tsx
import { useEffect, useRef } from 'react'
import { gsap } from 'gsap'
import { SplitText } from 'gsap/SplitText'

gsap.registerPlugin(SplitText)

interface SplitTextRevealProps {
  text: string
  type?: 'chars' | 'words' | 'lines'
  stagger?: number
  className?: string
}

export function SplitTextReveal({
  text,
  type = 'chars',
  stagger = 0.03,
  className = '',
}: SplitTextRevealProps) {
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!containerRef.current) return

    const ctx = gsap.context(() => {
      const split = new SplitText(containerRef.current!, {
        type,
        linesClass: 'split-line',
        wordsClass: 'split-word',
        charsClass: 'split-char',
      })

      const elements =
        type === 'chars'
          ? split.chars
          : type === 'words'
          ? split.words
          : split.lines

      gsap.from(elements, {
        y: '120%',
        opacity: 0,
        rotationX: -60,
        transformOrigin: '0% 50% -50',
        ease: 'back.out(1.5)',
        duration: 0.6,
        stagger,
        scrollTrigger: {
          trigger: containerRef.current,
          start: 'top 80%',
          once: true,
        },
      })
    }, containerRef)

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

  return (
    <div
      ref={containerRef}
      className={`overflow-hidden ${className}`}
      style={{ perspective: '600px' }}
    >
      {text}
    </div>
  )
}

Ручная реализация SplitText без платного плагина

// utils/split-text.ts
export function splitIntoSpans(
  element: HTMLElement,
  mode: 'chars' | 'words'
): HTMLElement[] {
  const text = element.textContent ?? ''
  const spans: HTMLElement[] = []

  element.innerHTML = ''

  const parts = mode === 'chars' ? text.split('') : text.split(/\s+/)

  parts.forEach((part, i) => {
    const span = document.createElement('span')
    span.textContent = mode === 'words' ? part : (part === ' ' ? '\u00A0' : part)
    span.style.display = 'inline-block'
    span.style.overflow = 'hidden'
    element.appendChild(span)

    if (mode === 'words' && i < parts.length - 1) {
      element.appendChild(document.createTextNode(' '))
    }

    spans.push(span)
  })

  return spans
}
// hooks/useTextReveal.ts
import { useEffect, useRef } from 'react'
import { splitIntoSpans } from '../utils/split-text'

export function useTextReveal(mode: 'chars' | 'words' = 'words') {
  const ref = useRef<HTMLElement>(null)

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

    const spans = splitIntoSpans(el, mode)

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (!entry.isIntersecting) return
        spans.forEach((span, i) => {
          span.animate(
            [
              { opacity: 0, transform: 'translateY(100%)' },
              { opacity: 1, transform: 'translateY(0%)' },
            ],
            {
              duration: 500,
              delay: i * (mode === 'chars' ? 30 : 80),
              easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
              fill: 'forwards',
            }
          )
        })
        observer.unobserve(el)
      },
      { threshold: 0.3 }
    )

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

  return ref
}

Glitch-эффект для заголовков

/* styles/glitch.css */
@keyframes glitch-1 {
  0%, 100% { clip-path: inset(0 0 95% 0); transform: translate(-3px, 0); }
  20% { clip-path: inset(20% 0 60% 0); transform: translate(3px, 0); }
  40% { clip-path: inset(50% 0 30% 0); transform: translate(-2px, 0); }
  60% { clip-path: inset(80% 0 5% 0); transform: translate(2px, 0); }
}

@keyframes glitch-2 {
  0%, 100% { clip-path: inset(50% 0 30% 0); transform: translate(3px, 0); color: #ff0070; }
  30% { clip-path: inset(10% 0 70% 0); transform: translate(-3px, 0); }
  70% { clip-path: inset(80% 0 5% 0); transform: translate(2px, 0); color: #00d4ff; }
}

.glitch {
  position: relative;
}

.glitch::before,
.glitch::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  animation-duration: 0.8s;
  animation-iteration-count: infinite;
  animation-timing-function: steps(1);
}

.glitch::before { animation-name: glitch-1; }
.glitch::after  { animation-name: glitch-2; }

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

Typed.js-эффект для одного заголовка — 2 часа. Посимвольные анимации для нескольких секций через Web Animations API — 1 рабочий день. Полная система с GSAP SplitText, wave-эффектами, glitch и scroll-триггерами — 2–3 рабочих дня.