Компоненты/Магнитная кнопка

Магнитная кнопка

27 сентября 2026

Кнопка тянется к курсору или пальцу и пружинит на место

Установка

npx astra-rn add magnetic-button
pnpm dlx astra-rn add magnetic-button
yarn dlx astra-rn add magnetic-button
bunx --bun astra-rn add magnetic-button

Файлы

index.tsx
НазадАнимированные ссылкиДалееМорфинг-кнопка

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

Вокруг кнопки есть невидимое поле радиусом range. Пока курсор в нём, кнопка смещается к нему на долю расстояния strength, а подпись — ещё чуть дальше (contentStrength), отчего кнопка кажется объёмной. Курсор ушёл — кнопка пружинит обратно с лёгким перелётом.

На телефоне магнит работает от пальца, а нажатие даёт отклик: кнопка сжимается до pressScale и срабатывает лёгкая вибрация (haptics, через expo-haptics; в вебе вибрация пропускается).

Вручную

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

npm install expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
pnpm add expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
yarn add expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets
bun add expo-haptics react-native-gesture-handler react-native-reanimated react-native-worklets

Скопируйте эти файлы в src/shared/components/micro-interactions/magnetic-button/:

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

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

import { useRef, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { StatusBar } from "expo-status-bar";
import { Ionicons } from "@expo/vector-icons";

import { MagneticButton, type MagneticButtonHandle } from "@/shared/components/micro-interactions/magnetic-button";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const CUSTOMIZE = [
  { key: "label", label: "Текст кнопки", type: "text", default: "Начать", maxLength: 16 },
  { key: "strength", label: "Сила притяжения", type: "slider", min: 0, max: 0.8, step: 0.05, default: 0.35 },
  { key: "contentStrength", label: "Сдвиг содержимого", type: "slider", min: 0, max: 0.6, step: 0.02, default: 0.18 },
  { key: "range", label: "Радиус магнита", type: "slider", min: 20, max: 140, step: 2, unit: "px", default: 70 },
  { key: "pressScale", label: "Сжатие при нажатии", type: "slider", min: 0.8, max: 1, step: 0.01, default: 0.93 },
  { key: "radius", label: "Скругление", type: "slider", min: 0, max: 32, step: 1, unit: "px", default: 20 },
  { key: "color", label: "Цвет кнопки", type: "color", default: "#f5f2ef" },
  { key: "textColor", label: "Цвет текста", type: "color", default: "#0a0a0a" },
] as const;

export default function App() {
  const custom = useCustomize(CUSTOMIZE);
  const primary = useRef<MagneticButtonHandle>(null);
  const [count, setCount] = useState(0);

  useAutoplay(async ({ wait }) => {
    for (const [x, y] of [
      [-90, -40],
      [80, -50],
      [95, 45],
      [-70, 55],
    ] as const) {
      primary.current?.attract(x, y);
      await wait(520);
    }
    primary.current?.release();
    await wait(1000);
  });

  return (
    <Showcase>
      <View style={styles.container}>
        <StatusBar style="light" />
        <MagneticButton
          ref={primary}
          label={custom.label}
          strength={custom.strength}
          contentStrength={custom.contentStrength}
          range={custom.range}
          pressScale={custom.pressScale}
          radius={custom.radius}
          color={custom.color}
          textColor={custom.textColor}
          onPress={() => setCount((value) => value + 1)}
        />
        <MagneticButton
          width={64}
          height={64}
          radius={32}
          range={custom.range * 0.8}
          strength={custom.strength}
          contentStrength={custom.contentStrength}
          pressScale={custom.pressScale}
          color="#1c1c1c"
          label="Избранное"
          onPress={() => setCount((value) => value + 1)}
        >
          <Ionicons name="heart" size={24} color="#f5f2ef" />
        </MagneticButton>
        <Text style={styles.counter}>Нажатий: {count}</Text>
      </View>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: "center", justifyContent: "center", overflow: "hidden" },
  counter: { color: "#8a8683", fontSize: 13, fontVariant: ["tabular-nums"] },
});

Пропсы

Пропс
Описание
label?

Тип string

children?

Тип ReactNode

onPress?

Тип () => void

width?

Тип number

height?

Тип number

radius?

Тип number

range?

Тип number

strength?

Тип number

contentStrength?

Тип number

pressScale?

Тип number

haptics?

Тип boolean

color?

Тип string

textColor?

Тип string

disabled?

Тип boolean

style?

Тип StyleProp<ViewStyle>

textStyle?

Тип StyleProp<TextStyle>

Методы через ref

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

Тип (x: number, y: number) => void

release

Тип () => void