Компоненты/Плавное появление

Плавное появление

21 августа 2026

Плавно показывает и убирает содержимое при входе и выходе из дерева

Установка

npx astra-rn add fade-component
pnpm dlx astra-rn add fade-component
yarn dlx astra-rn add fade-component
bunx --bun astra-rn add fade-component

Файлы

index.tsx
НазадРаскрывающаяся карточкаДалееВеерное меню

Вручную

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

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

Скопируйте эти файлы в src/shared/components/base/fade-component/:

Он импортирует ещё один модуль — astra-rn add кладёт их рядом сам, вручную скопируйте и их:

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

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

import React, { useCallback, useMemo, useRef, useState } from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

import { FadeComponent } from "@/shared/components/base/fade-component";
import type { TFadeState } from "@/shared/components/base/fade-component/types";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const PLANS = {
  from: { label: "Monthly", price: "$12", cadence: "per month" },
  to: { label: "Yearly", price: "$96", cadence: "per year" },
} as const;

const STATES: TFadeState[] = ["from", "to"];

const CUSTOMIZE = [
  { key: "fromLabel", label: "Вкладка 1", type: "text", default: PLANS.from.label, maxLength: 16 },
  { key: "fromPrice", label: "Цена 1", type: "text", default: PLANS.from.price, maxLength: 10 },
  { key: "fromCadence", label: "Период 1", type: "text", default: PLANS.from.cadence, maxLength: 20 },
  { key: "toLabel", label: "Вкладка 2", type: "text", default: PLANS.to.label, maxLength: 16 },
  { key: "toPrice", label: "Цена 2", type: "text", default: PLANS.to.price, maxLength: 10 },
  { key: "toCadence", label: "Период 2", type: "text", default: PLANS.to.cadence, maxLength: 20 },
  { key: "animation", label: "Анимация", type: "select", options: [{ value: "spring", label: "Пружина" }, { value: "timing", label: "Плавно" }], default: "spring" },
  { key: "duration", label: "Длительность", type: "slider", min: 100, max: 3000, step: 50, unit: "ms", default: 1000 },
  { key: "hiddenScale", label: "Масштаб скрытия", type: "slider", min: 0.5, max: 1, step: 0.01, default: 0.96 },
  { key: "priceColor", label: "Цвет цены", type: "color", default: "#FFFFFF" },
  { key: "accent", label: "Активная вкладка", type: "color", default: "#2A2A38" },
  { key: "holdMs", label: "Показ", type: "slider", min: 400, max: 5000, step: 50, unit: "ms", default: 1250 },
] as const;

type PlanView = { label: string; price: string; cadence: string };

const Plan: React.FC<{ plan: PlanView; priceColor: string }> = ({ plan, priceColor }) => (
  <View style={styles.card}>
    <Text style={[styles.price, { color: priceColor }]}>{plan.price}</Text>
    <Text style={styles.cadence}>{plan.cadence}</Text>
  </View>
);

const App: React.FC = (): React.ReactNode => {
  const [state, setState] = useState<TFadeState>("from");
  const custom = useCustomize(CUSTOMIZE);
  const holdRef = useRef(custom.holdMs);
  holdRef.current = custom.holdMs;
  const plans = useMemo<Record<TFadeState, PlanView>>(
    () => ({
      from: { label: custom.fromLabel, price: custom.fromPrice, cadence: custom.fromCadence },
      to: { label: custom.toLabel, price: custom.toPrice, cadence: custom.toCadence },
    }),
    [custom.fromLabel, custom.fromPrice, custom.fromCadence, custom.toLabel, custom.toPrice, custom.toCadence],
  );

  const onChange = useCallback((next: TFadeState): void => {
    setState(next);
  }, []);

  useAutoplay(async ({ wait }) => {
    await wait(400);
    setState("to");
    await wait(holdRef.current);
    setState("from");
    await wait(900);
  });

  return (
    <Showcase>
      <SafeAreaView style={styles.container}>
        <FadeComponent
          state={state}
          animation={custom.animation as "spring" | "timing"}
          duration={custom.duration}
          onChange={onChange}
          style={styles.fade}
        >
          <FadeComponent.From hiddenScale={custom.hiddenScale}>
            <Plan plan={plans.from} priceColor={custom.priceColor} />
          </FadeComponent.From>
          <FadeComponent.To hiddenScale={custom.hiddenScale}>
            <Plan plan={plans.to} priceColor={custom.priceColor} />
          </FadeComponent.To>
        </FadeComponent>

        <View style={styles.segment}>
          {STATES.map((value) => {
            const active = state === value;

            return (
              <Pressable
                key={value}
                style={[styles.segmentItem, active && styles.segmentItemActive, active && { backgroundColor: custom.accent }]}
                onPress={() => setState(value)}
              >
                <Text
                  style={[
                    styles.segmentText,
                    active && styles.segmentTextActive,
                  ]}
                >
                  {plans[value].label}
                </Text>
              </Pressable>
            );
          })}
        </View>
      </SafeAreaView>
    </Showcase>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#0a0a0f",
    justifyContent: "center",
    alignItems: "center",
    paddingHorizontal: 24,
  },
  fade: {
    width: 260,
    height: 160,
  },
  card: {
    width: 260,
    height: 160,
    borderRadius: 20,
    borderWidth: StyleSheet.hairlineWidth,
    justifyContent: "center",
    alignItems: "center",
  },
  price: {
    fontSize: 40,
    fontWeight: "700",
    color: "#FFFFFF",
    letterSpacing: -1,
  },
  cadence: {
    marginTop: 6,
    fontSize: 13,
    color: "#7A7A8A",
  },
  segment: {
    flexDirection: "row",
    marginTop: 24,
    padding: 4,
    borderRadius: 999,
    backgroundColor: "#16161F",
  },
  segmentItem: {
    paddingHorizontal: 22,
    paddingVertical: 10,
    borderRadius: 999,
  },
  segmentItemActive: {
    backgroundColor: "#2A2A38",
  },
  segmentText: {
    fontSize: 13,
    fontWeight: "600",
    color: "#7A7A8A",
  },
  segmentTextActive: {
    color: "#FFFFFF",
  },
});

export default App;

Пропсы

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

Тип ReactNode

duration?

Duration in ms for the `timing` animation. @default 1000

Тип number

delay?

Delay in ms before the animation starts. @default 0

Тип number

easing?

Easing for the `timing` animation.

Тип EasingFunction | EasingFunctionFactory

animation?

Drive the crossfade with timing or spring. @default "timing"

Тип TFadeAnimation

springConfig?

Spring config used when `animation` is `"spring"`.

Тип WithSpringConfig

state?

Controlled face — when provided the component mirrors this value.

Тип TFadeState

defaultState?

Uncontrolled starting face. @default "from"

Тип TFadeState

onChange?

Fired when a transition is requested.

Тип (state: TFadeState) => void

onAnimationEnd?

Fired when a transition settles.

Тип (state: TFadeState) => void

disabled?

Ignore imperative calls and controlled updates. @default false

Тип boolean

style?

Style applied to the wrapping container.

Тип StyleProp<ViewStyle>

blockHiddenTouches?

Prevent touches from reaching the face that is fading out. @default true

Тип boolean

testID?

Тип string