Реалізація анімації тексту (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 або ручна реалізація). Перший клас створює ілюзію введення в реальному часі. Другий дозволяє анімувати кожну букву або слово незалежно — хвильові ефекти, 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, хвильовими ефектами, glitch та scroll-триггерами — 2–3 робочих дні.