Компоненты/Превью ссылки

Превью ссылки

28 сентября 2026

Ссылки, которые по долгому нажатию показывают картинку-подсказку, следующую за пальцем с лёгким наклоном; на вебе — наведением.

Установка

npx astra-rn add link-preview
pnpm dlx astra-rn add link-preview
yarn dlx astra-rn add link-preview
bunx --bun astra-rn add link-preview

Файлы

index.tsx
НазадТурнирная сетка плей-оффДалееРтуть

Ссылка прямо в тексте, которая показывает над собой карточку с картинкой. На телефоне карточка появляется по долгому нажатию и ведётся пальцем, на вебе — при наведении курсора. Карточка пружинисто следует за указателем по горизонтали, наклоняется в сторону движения, а картинка внутри слегка смещается навстречу — получается эффект параллакса. Короткий тап открывает href.

Оберните текст в LinkPreviewProvider, чтобы карточка рисовалась поверх соседних строк и не обрезалась. Пружины и наклон считаются на UI-потоке Reanimated; при «Уменьшении движения» наклон и параллакс отключаются, остаётся простое появление. Работает на iOS, Android и в вебе.

Вручную

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

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

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

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

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

import React from "react";
import { StyleSheet, Text, View, useWindowDimensions } from "react-native";

import { LinkPreview, LinkPreviewProvider } from "@/shared/components/molecules/link-preview";
import { PHOTO_SOURCES } from "~/assets/photos";
import { Showcase, useCustomize } from "~/showcase";

const TITLE_SIZE = 36;

const CUSTOMIZE = [
  { key: "titleSize", label: "Размер заголовка", type: "slider", min: 20, max: 64, step: 1, unit: "px", default: TITLE_SIZE },
  { key: "kicker", label: "Надпись сверху", type: "text", default: "Сейчас читаю", maxLength: 40 },
  { key: "title", label: "Заголовок", type: "text", default: "Шпион × Семья", maxLength: 40 },
  { key: "width", label: "Ширина превью", type: "slider", min: 120, max: 400, step: 10, unit: "px", default: 260 },
  { key: "height", label: "Высота превью", type: "slider", min: 90, max: 320, step: 10, unit: "px", default: 190 },
  { key: "radius", label: "Скругление превью", type: "slider", min: 0, max: 32, step: 1, unit: "px", default: 12 },
  { key: "tilt", label: "Наклон при движении", type: "slider", min: 0, max: 20, step: 1, unit: "°", default: 8 },
  {
    key: "photos",
    label: "Картинки",
    type: "images",
    count: 4,
    default: [
      PHOTO_SOURCES["cinematic-carousel-2"],
      PHOTO_SOURCES["property-detail-v1-7"],
      PHOTO_SOURCES["property-detail-v1-8"],
      PHOTO_SOURCES["property-detail-v1-6"],
    ],
  },
] as const;

export default function LinkPreviewScreen() {
  const custom = useCustomize(CUSTOMIZE);
  const { width } = useWindowDimensions();
  const [titlePhoto, levPhoto, anyaPhoto, yaraPhoto] = custom.photos;
  const shared = { radius: custom.radius, tilt: custom.tilt };
  const titleSize = custom.titleSize === TITLE_SIZE ? (width >= 640 ? 36 : 30) : custom.titleSize;

  return (
    <Showcase>
      <LinkPreviewProvider style={styles.page}>
        <View style={styles.frame}>
          <View style={styles.inner}>
            <View style={styles.heading}>
              <Text style={styles.kicker}>{custom.kicker.toUpperCase()}</Text>
              <LinkPreview
                href="#"
                src={titlePhoto!}
                alt={custom.title}
                width={custom.width}
                height={custom.height}
                {...shared}
                style={[styles.title, { fontSize: titleSize, lineHeight: titleSize * 1.2, letterSpacing: -0.025 * titleSize }]}
              >
                {custom.title}
              </LinkPreview>
            </View>
            <Text style={styles.paragraph}>
              Чтобы сохранить мир, лучший разведчик под кодовым именем{" "}
              <LinkPreview href="#" src={levPhoto!} alt="Лев Форов" {...shared} style={styles.inline}>
                Лев
              </LinkPreview>{" "}
              должен создать фиктивную семью и проникнуть в элитную школу. Сам того не зная, он берёт в дочери девочку-телепата по имени{" "}
              <LinkPreview href="#" src={anyaPhoto!} alt="Аня Форова" {...shared} style={styles.inline}>
                Аня
              </LinkPreview>{" "}
              и в жёны профессиональную убийцу по имени{" "}
              <LinkPreview href="#" src={yaraPhoto!} alt="Яра Форова" {...shared} style={styles.inline}>
                Яра
              </LinkPreview>
              . Вместе они разбираются с «мелочами» повседневной жизни, скрывая свои тайны друг от друга.
            </Text>
          </View>
        </View>
      </LinkPreviewProvider>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  page: { flex: 1, backgroundColor: "#0a0a0a" },
  frame: { flex: 1, justifyContent: "center", paddingVertical: 48 },
  inner: { alignItems: "center", gap: 40, paddingHorizontal: 24, paddingVertical: 48 },
  heading: { alignItems: "center" },
  kicker: {
    marginBottom: 12,
    fontSize: 12,
    lineHeight: 16,
    fontWeight: "500",
    letterSpacing: 2.16,
    color: "#a1a1a1",
  },
  title: { fontWeight: "500" },
  paragraph: {
    maxWidth: 672,
    textAlign: "center",
    fontSize: 18,
    lineHeight: 29.25,
    color: "rgba(250,250,250,0.7)",
  },
  inline: { fontSize: 18, lineHeight: 29.25 },
});

Пропсы

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

Тип string

src

Тип LinkPreviewSource

alt?

Тип string

width?

Тип number

height?

Тип number

radius?

Тип number

tilt?

Тип number

children

Тип ReactNode

style?

Тип StyleProp<TextStyle>

containerStyle?

Тип StyleProp<ViewStyle>

onPress?

Тип () => void