Компоненты/Веерное меню

Веерное меню

21 августа 2026

Радиальное меню, элементы которого раскрываются веером из своей кнопки

Установка

npx astra-rn add fan-menu
pnpm dlx astra-rn add fan-menu
yarn dlx astra-rn add fan-menu
bunx --bun astra-rn add fan-menu

Файлы

index.tsx
НазадПлавное появлениеДалееЧастые вопросы

Вручную

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

npm install @expo/vector-icons expo-blur react-native-reanimated
pnpm add @expo/vector-icons expo-blur react-native-reanimated
yarn add @expo/vector-icons expo-blur react-native-reanimated
bun add @expo/vector-icons expo-blur react-native-reanimated

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

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

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

import React, { useRef, useState } from "react";
import { Platform, StyleSheet, View, useWindowDimensions } from "react-native";
import { Feather } from "@expo/vector-icons";
import { FanMenu, FanItemDirection, FanPosition } from "@/shared/components/molecules/fan-menu";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const ICON_STYLE = { width: 15 } as const;

const isWeb = Platform.OS === "web";

const ITEMS = [
  { value: "learning", label: "Learning", icon: "book-open" },
  { value: "document", label: "Document", icon: "file-text" },
  { value: "music", label: "Music", icon: "music" },
  { value: "video", label: "Video", icon: "film" },
  { value: "image", label: "Image", icon: "image" },
] as const;

const CUSTOMIZE = [
  {
    key: "label1",
    label: "Пункт 1",
    type: "text",
    default: "Learning",
    maxLength: 16,
  },
  {
    key: "label2",
    label: "Пункт 2",
    type: "text",
    default: "Document",
    maxLength: 16,
  },
  {
    key: "label3",
    label: "Пункт 3",
    type: "text",
    default: "Music",
    maxLength: 16,
  },
  {
    key: "label4",
    label: "Пункт 4",
    type: "text",
    default: "Video",
    maxLength: 16,
  },
  {
    key: "label5",
    label: "Пункт 5",
    type: "text",
    default: "Image",
    maxLength: 16,
  },
  { key: "iconColor", label: "Цвет иконок", type: "color", default: "#1d1d1f" },
  {
    key: "itemDirection",
    label: "Сторона",
    type: "select",
    options: [
      { value: "left", label: "Влево" },
      { value: "right", label: "Вправо" },
      { value: "none", label: "По центру" },
    ],
    default: "left",
  },
  {
    key: "spread",
    label: "Разброс",
    type: "slider",
    min: 0,
    max: 20,
    step: 0.5,
    default: 5,
  },
  {
    key: "spacing",
    label: "Шаг",
    type: "slider",
    min: 40,
    max: 120,
    step: 1,
    unit: "px",
    default: 70,
  },
  {
    key: "tilt",
    label: "Наклон",
    type: "slider",
    min: 0,
    max: 30,
    step: 1,
    unit: "°",
    default: 6,
  },
  {
    key: "stagger",
    label: "Каскад",
    type: "slider",
    min: 0,
    max: 150,
    step: 5,
    unit: "ms",
    default: 25,
  },
  {
    key: "holdMs",
    label: "Показ",
    type: "slider",
    min: 600,
    max: 5000,
    step: 100,
    unit: "ms",
    default: 1700,
  },
] as const;

export default function FanMenuScreen() {
  const [open, setOpen] = useState<boolean>(false);
  const custom = useCustomize(CUSTOMIZE);
  const holdRef = useRef(custom.holdMs);
  holdRef.current = custom.holdMs;
  const labels = [
    custom.label1,
    custom.label2,
    custom.label3,
    custom.label4,
    custom.label5,
  ];
  const { height } = useWindowDimensions();
  const lift = Math.max(120, Math.min(400, height - 385));

  useAutoplay(async ({ wait }) => {
    await wait(300);
    setOpen(true);
    await wait(holdRef.current);
    setOpen(false);
    await wait(1200);
  });

  return (
    <Showcase>
      <View style={styles.stage} pointerEvents="box-none">
        <FanMenu
          open={open}
          onOpenChange={setOpen}
          position={FanPosition.BottomCenter}
          itemDirection={custom.itemDirection as FanItemDirection}
          spread={custom.spread}
          spacing={custom.spacing}
          tilt={custom.tilt}
          style={{
            bottom: lift,
          }}
          stagger={custom.stagger}
        >
          {ITEMS.map((item, index) => (
            <FanMenu.Item
              key={item.value}
              value={item.value}
              style={{
                paddingHorizontal: 12,
              }}
              onPress={isWeb ? undefined : (v) => alert(v + " Pressed")}
            >
              <FanMenu.Icon>
                <Feather
                  name={item.icon}
                  size={15}
                  color={custom.iconColor}
                  style={ICON_STYLE}
                />
              </FanMenu.Icon>
              <FanMenu.Label>{labels[index]}</FanMenu.Label>
            </FanMenu.Item>
          ))}
        </FanMenu>
      </View>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  stage: {
    ...StyleSheet.absoluteFillObject,
    overflow: "hidden",
  },
});

Пропсы

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

Тип ReactNode

open?

Тип boolean

defaultOpen?

Тип boolean

onOpenChange?

Тип (open: boolean) => void

onOpen?

Тип () => void

onClose?

Тип () => void

position?

Тип TFanPosition

offset?

Тип TFanOffset

direction?

Тип TFanDirection

itemDirection?

Тип TItemDirection

spacing?

Тип number

spread?

Тип number

tilt?

Тип number

stagger?

Тип number

springConfig?

Тип WithSpringConfig

buttonSize?

Тип number

closeOnBackdropPress?

Тип boolean

style?

Тип StyleProp<ViewStyle>