Компоненты/Раскладное меню

Раскладное меню

21 августа 2026

Меню раскладывает элементы поворотом с задержкой

Установка

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

Файлы

index.tsx
НазадПодносДалееСфера

Вручную

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

npm install react-native-reanimated react-native-safe-area-context react-native-worklets
pnpm add react-native-reanimated react-native-safe-area-context react-native-worklets
yarn add react-native-reanimated react-native-safe-area-context react-native-worklets
bun add react-native-reanimated react-native-safe-area-context react-native-worklets

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

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

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

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

import React, { useMemo, useRef, type RefObject } from "react";
import { StyleSheet, View } from "react-native";
import { Feather } from "@expo/vector-icons";
import { UnfoldMenu } from "@/shared/components/organisms/unfold-menu";
import { useUnfoldMenu } from "@/shared/components/organisms/unfold-menu/context";
import { Showcase, useAutoplay, useCustomize } from "~/showcase";

const ITEMS = [
  { value: "search", label: "Search", icon: "search" },
  { value: "share", label: "Share", icon: "share-2" },
  { value: "copy", label: "Copy", icon: "copy" },
  { value: "edit", label: "Edit", icon: "edit-2" },
  { value: "archive", label: "Archive", icon: "archive" },
  { value: "delete", label: "Delete", icon: "trash-2" },
] as const;

const CUSTOMIZE = [
  { key: "triggerLabel", label: "Кнопка", type: "text", default: "Add Task", maxLength: 24 },
  { key: "title", label: "Заголовок", type: "text", default: "Create", maxLength: 24 },
  { key: "theme", label: "Тема", type: "select", options: [{ value: "dark", label: "Тёмная" }, { value: "light", label: "Светлая" }], default: "dark" },
  { key: "surface", label: "Фон панели", type: "color", default: "#171716" },
  { key: "text", label: "Цвет текста", type: "color", default: "#f6f3ec" },
  { key: "columns", label: "Колонки", type: "slider", min: 2, max: 4, step: 1, default: 3 },
  { key: "items", label: "Пунктов", type: "slider", min: 2, max: 6, step: 1, default: 6 },
  { key: "radius", label: "Скругление", type: "slider", min: 0, max: 32, step: 1, unit: "px", default: 16 },
  { key: "iconSize", label: "Размер иконок", type: "slider", min: 14, max: 30, step: 1, unit: "px", default: 20 },
  { key: "hold", label: "Показ меню", type: "slider", min: 800, max: 5000, step: 100, unit: "ms", default: 1900 },
  { key: "closeOnSelect", label: "Закрывать выбором", type: "toggle", default: true },
] as const;

const DARK_DEFAULTS = { surface: "#171716", text: "#f6f3ec" };

type TMenuRemote = ReturnType<typeof useUnfoldMenu>;

function MenuRemote({ remote }: { remote: RefObject<TMenuRemote | null> }) {
  const menu = useUnfoldMenu("MenuRemote");
  remote.current = menu;
  return null;
}

export default function UnfoldMenuScreen() {
  const menu = useRef<TMenuRemote | null>(null);
  const custom = useCustomize(CUSTOMIZE);
  const holdRef = useRef(custom.hold);
  holdRef.current = custom.hold;
  const theme = custom.theme as "dark" | "light";
  const { surface, text } = custom;
  const palette = useMemo(
    () => ({
      ...(surface !== DARK_DEFAULTS.surface && { surface }),
      ...(text !== DARK_DEFAULTS.text && { text }),
    }),
    [surface, text],
  );

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

  return (
    <Showcase>
      <View style={styles.center}>
        <UnfoldMenu
          theme={theme}
          palette={palette}
          radius={custom.radius}
          iconSize={custom.iconSize}
          closeOnSelect={custom.closeOnSelect}
          onSelect={(value) => console.log(value)}
        >
          <MenuRemote remote={menu} />
          <UnfoldMenu.Trigger>
            <UnfoldMenu.Label>{custom.triggerLabel}</UnfoldMenu.Label>
            <UnfoldMenu.Icon>
              {({ color, size }) => (
                <Feather name="plus" size={size} color={color} />
              )}
            </UnfoldMenu.Icon>
          </UnfoldMenu.Trigger>

          <UnfoldMenu.Content>
            <UnfoldMenu.Header>
              <UnfoldMenu.Title>{custom.title}</UnfoldMenu.Title>
              <UnfoldMenu.Close>
                {({ color, size }) => (
                  <Feather name="x" size={size} color={color} />
                )}
              </UnfoldMenu.Close>
            </UnfoldMenu.Header>

            <UnfoldMenu.Grid columns={custom.columns}>
              {ITEMS.slice(0, custom.items).map((item) => (
                <UnfoldMenu.Item key={item.value} value={item.value}>
                  <UnfoldMenu.Icon>
                    {({ color, size }) => (
                      <Feather name={item.icon} size={size} color={color} />
                    )}
                  </UnfoldMenu.Icon>
                  <UnfoldMenu.Label>{item.label}</UnfoldMenu.Label>
                </UnfoldMenu.Item>
              ))}
            </UnfoldMenu.Grid>
          </UnfoldMenu.Content>
        </UnfoldMenu>
      </View>
    </Showcase>
  );
}

const styles = StyleSheet.create({
  center: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    overflow: "hidden",
  },
});

Пропсы

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

Тип ReactNode

open?

Тип boolean

defaultOpen?

Тип boolean

onOpenChange?

Тип (open: boolean) => void

onSelect?

Тип (value?: string) => void

closeOnSelect?

Тип boolean

panelWidth?

Тип number

screenMargin?

Тип number

radius?

Тип number

iconSize?

Тип number

theme?

Тип TUnfoldMenuTheme

palette?

Тип Partial<IUnfoldMenuPalette>

style?

Тип StyleProp<ViewStyle>