Компоненты/Текст по знакам

Текст по знакам

7 февраля 2026

Плавная посимвольная анимация текста на Skia

Установка

npx astra-rn add staggered-text
pnpm dlx astra-rn add staggered-text
yarn dlx astra-rn add staggered-text
bunx --bun astra-rn add staggered-text

Файлы

index.tsx
НазадТабло вылетовДалееТекст из плашек

Вручную

Поставьте зависимости:

npm install @shopify/react-native-skia react-native-reanimated
pnpm add @shopify/react-native-skia react-native-reanimated
yarn add @shopify/react-native-skia react-native-reanimated
bun add @shopify/react-native-skia react-native-reanimated

Скопируйте эти файлы в src/shared/components/organisms/staggered-text/:

Проверьте алиас путей. Импорты в коде выше записаны так, как их пишет astra-rn add: @/shared/components, @/shared/utils и @/shared/hooks. Нужен алиас "@/*": ["./src/*"] в tsconfig.json — или поправьте импорты под свои пути.

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

import React, { useEffect, useMemo, useState } from "react";
import { StyleSheet, View, useWindowDimensions } from "react-native";
import { StaggeredText } from "@/shared/components/organisms/staggered-text";
import type { TStaggerFrom } from "@/shared/components/organisms/staggered-text/types";
import { Showcase, useCustomize } from "~/showcase";

const TEXTS: string[] = [
  "Do you love Astra!",
  "Isn't it amazing?",
  "Try it out now!",
];

const ANIMATION_CONFIG = { characterDelay: 50 };

const ENTER_TO = { translateY: 1, opacity: 0, blur: 10, scale: 0.1 };

const EXIT_TO = { translateY: -1, opacity: 0, blur: 10, scale: 0 };

const CUSTOMIZE = [
  { key: "text1", label: "Фраза 1", type: "text", default: TEXTS[0], maxLength: 40 },
  { key: "text2", label: "Фраза 2", type: "text", default: TEXTS[1], maxLength: 40 },
  { key: "text3", label: "Фраза 3", type: "text", default: TEXTS[2], maxLength: 40 },
  { key: "color", label: "Цвет текста", type: "color", default: "#ffffff" },
  { key: "fontSize", label: "Размер шрифта", type: "slider", min: 16, max: 64, step: 1, unit: "px", default: 35 },
  { key: "letterSpacing", label: "Межбуквенный", type: "slider", min: -4, max: 10, step: 0.5, unit: "px", default: 1 },
  {
    key: "staggerFrom",
    label: "Порядок букв",
    type: "select",
    options: [
      { value: "leading", label: "С начала" },
      { value: "trailing", label: "С конца" },
      { value: "center", label: "От центра" },
      { value: "edges", label: "С краёв" },
      { value: "random", label: "Случайно" },
    ],
    default: "trailing",
  },
  { key: "characterDelay", label: "Задержка букв", type: "slider", min: 0, max: 100, step: 1, unit: "ms", default: 50 },
  { key: "blur", label: "Размытие", type: "slider", min: 0, max: 30, step: 1, default: 10 },
  { key: "interval", label: "Смена фразы", type: "slider", min: 800, max: 6000, step: 100, unit: "ms", default: 2000 },
] as const;

export default function App(): React.ReactElement {
  const [index, setIndex] = useState<number>(0);
  const custom = useCustomize(CUSTOMIZE);
  const { text1, text2, text3, characterDelay, blur } = custom;
  const texts = useMemo(
    () =>
      text1 === TEXTS[0] && text2 === TEXTS[1] && text3 === TEXTS[2]
        ? TEXTS
        : [text1, text2, text3],
    [text1, text2, text3],
  );
  const animationConfig = useMemo(
    () => (characterDelay === 50 ? ANIMATION_CONFIG : { characterDelay }),
    [characterDelay],
  );
  const { width: screenWidth } = useWindowDimensions();
  const longest = Math.max(1, ...texts.map((text) => text.length));
  const fontSize = Math.max(
    14,
    Math.min(custom.fontSize, Math.floor(((screenWidth - 32) / longest - custom.letterSpacing) / 0.52)),
  );
  const enterTo = useMemo(() => (blur === 10 ? ENTER_TO : { ...ENTER_TO, blur }), [blur]);
  const exitTo = useMemo(() => (blur === 10 ? EXIT_TO : { ...EXIT_TO, blur }), [blur]);

  useEffect(() => {
    const interval = setInterval(() => {
      setIndex((prevIndex) => (prevIndex + 1) % TEXTS.length);
    }, custom.interval);

    return () => clearInterval(interval);
  }, [custom.interval]);

  return (
    <Showcase>
      <View style={styles.container}>
        <StaggeredText
          texts={texts}
          activeIndex={index}
          fontSize={fontSize}
          height={500}
          animationConfig={animationConfig}
          enterTo={enterTo}
          exitTo={exitTo}
          color={custom.color}
          letterSpacing={custom.letterSpacing}
          staggerFrom={custom.staggerFrom as TStaggerFrom}
        />
      </View>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0b0b0e",
    justifyContent: "center",
    alignItems: "center",
    overflow: "hidden",
  },
});

Пропсы

Пропс
Описание
texts

Тип string[]

activeIndex?

Тип number

fontSize?

Тип number

color?

Тип string

fontPath?

Тип ReturnType<typeof require>

height?

Тип number

staggerFrom?

Тип TStaggerFrom

letterSpacing?

Тип number

enterFrom?

Тип ICharacterAnimationParams

enterTo?

Тип ICharacterAnimationParams

exitFrom?

Тип ICharacterAnimationParams

exitTo?

Тип ICharacterAnimationParams

animationConfig?

Тип IAnimationConfig

IStaggeredCharacterLayer

Пропс
Описание
texts

Тип string[]

activeIndex

Тип number

fontSize

Тип number

color

Тип string

font

Тип NonNullable<T>

height

Тип number

staggerFrom

Тип TStaggerFrom

enterFrom

Тип TResolvedChar

enterTo

Тип TResolvedChar

exitFrom

Тип TResolvedChar

exitTo

Тип TResolvedChar

config

Тип TResolvedConfig

letterSpacing

Тип number