Компоненты/Текст под прокрутку

Текст под прокрутку

27 сентября 2026

Слова загораются одно за другим по мере прокрутки

Установка

npx astra-rn add scroll-lit-text
pnpm dlx astra-rn add scroll-lit-text
yarn dlx astra-rn add scroll-lit-text
bunx --bun astra-rn add scroll-lit-text

Файлы

index.tsx
НазадБуквы и иконки на прокруткеДалееВолна блика

Как это работает

Передайте в компонент scrollY — общее значение из useAnimatedScrollHandler вашего списка. Компонент сам измеряет своё положение на экране и считает прогресс: слова начинают загораться, когда верх текста пересекает линию start (доля высоты экрана), и загорается всё, когда низ текста доходит до линии end. softness — сколько слов одновременно находятся в полутени.

Работает с любым Animated.ScrollView или Animated.FlatList. Если список не на весь экран, укажите viewportTop и viewportHeight. Меняется только прозрачность слов — раскладка стабильна.

Вручную

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

npm install react-native-reanimated
pnpm add react-native-reanimated
yarn add react-native-reanimated
bun add react-native-reanimated

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

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

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

import { useState } from "react";
import { StyleSheet, View, useWindowDimensions } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { StatusBar } from "expo-status-bar";
import { useFonts } from "expo-font";
import sfProRounded from "@/assets/fonts/sf-pro-rounded.ttf";
import Animated, {
  scrollTo,
  useAnimatedRef,
  useAnimatedScrollHandler,
  useDerivedValue,
  useSharedValue,
  withTiming,
  Easing,
} from "react-native-reanimated";

import { ScrollLitText } from "@/shared/components/molecules/scroll-lit-text";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const CUSTOMIZE = [
  { key: "text", label: "Текст", type: "text", default: "Хороший интерфейс не спорит с человеком. Он замечает паузу, подсказывает шаг и уходит в тень, когда дело сделано.", maxLength: 220 },
  { key: "fontSize", label: "Размер шрифта", type: "slider", min: 16, max: 44, step: 1, unit: "px", default: 30 },
  { key: "dimOpacity", label: "Яркость до", type: "slider", min: 0, max: 0.6, step: 0.01, default: 0.16 },
  { key: "softness", label: "Мягкость фронта", type: "slider", min: 0.2, max: 6, step: 0.1, unit: "сл.", default: 1.6 },
  { key: "color", label: "Цвет текста", type: "color", default: "#FAFAFA" },
  { key: "background", label: "Фон", type: "color", default: "#0A0A0A" },
] as const;

export default function App() {
  const [fontLoaded] = useFonts({
    SfProRounded: sfProRounded,
  });
  const custom = useCustomize(CUSTOMIZE);
  const { height } = useWindowDimensions();
  const scrollY = useSharedValue(0);
  const scrollRef = useAnimatedRef<Animated.ScrollView>();
  const driven = useSharedValue(-1);
  const [textHeight, setTextHeight] = useState(200);
  const target = Math.max(0, height * 0.9 - (height - textHeight) / 2);
  const litEnd = Math.min(0.8, 0.5 + textHeight / (2 * height) + 0.04);

  const onScroll = useAnimatedScrollHandler((event) => {
    scrollY.value = event.contentOffset.y;
  });

  useDerivedValue(() => {
    if (driven.value >= 0) scrollTo(scrollRef, 0, driven.value, false);
  });

  useAutoplay(async ({ wait }) => {
    driven.value = 0;
    await wait(500);
    driven.value = withTiming(target, { duration: 3600, easing: Easing.inOut(Easing.cubic) });
    await wait(4400);
    driven.value = withTiming(0, { duration: 1400, easing: Easing.inOut(Easing.cubic) });
    await wait(1800);
  }, { startDelayMs: 300 });

  return (
    <Showcase>
      <GestureHandlerRootView style={[styles.container, { backgroundColor: custom.background }]}>
        <StatusBar style="light" />
        <Animated.ScrollView
          ref={scrollRef}
          onScroll={onScroll}
          scrollEventThrottle={16}
          onTouchStart={() => {
            driven.value = -1;
          }}
          contentContainerStyle={[styles.content, { paddingTop: height * 0.9, paddingBottom: height * 0.7 }]}
          showsVerticalScrollIndicator={false}
        >
          <View onLayout={(event) => setTextHeight(event.nativeEvent.layout.height)}>
            <ScrollLitText
              text={custom.text}
              scrollY={scrollY}
              end={litEnd}
              fontSize={custom.fontSize}
              dimOpacity={custom.dimOpacity}
              softness={custom.softness}
              color={custom.color}
              fontFamily={fontLoaded ? "SfProRounded" : undefined}
            />
          </View>
          <View style={styles.rule} />
        </Animated.ScrollView>
      </GestureHandlerRootView>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: "#0A0A0A" },
  content: { paddingHorizontal: 24, gap: 18 },
  rule: { height: 1, backgroundColor: "#27272A", marginTop: 12 },
});

Пропсы

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

Тип string

scrollY

Тип SharedValue<number>

start?

Тип number

end?

Тип number

viewportTop?

Тип number

viewportHeight?

Тип number

softness?

Тип number

dimOpacity?

Тип number

color?

Тип string

fontSize?

Тип number

lineHeight?

Тип number

fontWeight?

Тип TextStyle["fontWeight"]

fontFamily?

Тип string

align?

Тип ScrollLitTextAlign

style?

Тип StyleProp<TextStyle>

containerStyle?

Тип StyleProp<ViewStyle>