suiss/uim · Interaction

Preview Menu

Long press an element to lift an enlarged preview of it above a dimmed screen, with a menu of actions below.

Preview Menu is an open-source mobile interaction from suiss/uim for React Native and Expo, with SwiftUI and Jetpack Compose versions built from the same design tokens. The @esuiss/uim CLI copies its source into your project, so you own the code.

iOS
Preview Menu (open by default) on iOS in light mode

Long pressing a PreviewMenuTrigger measures it and the menu, then animates its content from its place on screen into a larger preview, up to 360 points wide. The preview and the menu below it are centered together between the top and bottom safe areas. The rest of the screen fades behind a scrim, and the menu grows out of the trigger into place.

The original trigger is hidden while lifted, so the preview appears to rise out of it. With custom preview content, the trigger's content cross-fades into it as it grows. Pressing outside closes the menu and the preview and menu shrink back into the trigger. Use it for photos, messages or cards that benefit from a closer look before acting.

Installation

$ pnpm dlx @esuiss/uim@latest add preview-menu

The CLI also adds what it builds on: Icon, Text.

Manual

  1. Install the dependencies:
npx expo install cn react-native-reanimated react-native-safe-area-context react-native-screens react-native-worklets
  1. Add the pieces it builds on: Icon, Text, and the primitives components/primitives/preview-menu.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/interactions/preview-menu.tsx:

uniwind/components/interactions/preview-menu.tsx
import { cn } from "cn"
import * as React from "react"
import { Platform, View } from "react-native"
import { FadeIn, FadeOut, ReduceMotion } from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens"
import * as PreviewMenuPrimitive from "@/components/primitives/preview-menu"
import { resolveClassName } from "@/components/primitives/use-render"
import { AnimatedView } from "@/components/ui/animated-view"
import { IconClassContext } from "@/components/ui/icon"
import { Text, TextClassContext } from "@/components/ui/text"

const FullWindowOverlay =
  Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment

function usePreviewMenu() {
  1. Update the import paths to match your project.

Usage

import { PreviewMenu, PreviewMenuTrigger, PreviewMenuContent, PreviewMenuItem, PreviewMenuSeparator } from "@/components/interactions/preview-menu"
<PhotoGrid openIndex={1} />

Put a PreviewMenuTrigger with the element and a PreviewMenuContent with PreviewMenuItems inside PreviewMenu. By default the preview repeats the trigger's children; pass preview to show different content, such as a larger image.

Set aspectRatio to size the preview, for example 4 / 3. The menu can be uncontrolled or controlled with open, defaultOpen and onOpenChange. usePreviewMenu returns open, lifted and frame.

Examples

Default

iOS
Preview Menu on iOS in light mode

Disabled item

Includes a disabled menu item.

iOS
Preview Menu (disabled item) on iOS in light mode

Custom preview

Shows custom preview content with a 4 / 3 aspect ratio.

iOS
Preview Menu (custom preview) on iOS in light mode

Custom preview open

The custom preview, open on first render.

iOS
Preview Menu (custom preview open) on iOS in light mode

API Reference

PreviewMenu

The root that owns the open state and preview size.

PropType
About
Controls whether the preview is open.
Type
boolean
Default
—
About
Whether the preview starts open when uncontrolled.
Type
boolean
Default
—
About
Called when the preview opens or closes.
Type
(open: boolean) => void
Default
—
About
Width-to-height ratio of the lifted preview.
Type
number
Default
—
Type
React.ReactNode
Default
—

PreviewMenuTrigger

The element that opens the preview on long press.

ExtendsReact.ComponentProps<typeof PreviewMenuPrimitive.Trigger>

PreviewMenuContent

The scrim, the lifted preview and the menu.

ExtendsReact.ComponentProps<typeof PreviewMenuPrimitive.Popup>
PropType
About
Content to show in the preview instead of the trigger's children.
Type
React.ReactNode
Default
—
About
Classes for the preview container.
Type
string
Default
—
About
Name of a portal host to render into.
Type
string
Default
—

PreviewMenuItem

A menu action.

ExtendsReact.ComponentProps<typeof PreviewMenuPrimitive.Item>
PropType
About
default or destructive, which uses the danger color.
Type
"default""destructive"
Default
"default"
Type
React.ReactNode
Default
—

PreviewMenuSeparator

A thick divider between groups of items.

ExtendsReact.ComponentProps<typeof PreviewMenuPrimitive.Separator>

Accessibility

  • The trigger has a longpress accessibility action, so screen reader users can open the menu without holding.
  • The trigger exposes expanded and disabled states.
  • The preview is hidden from screen readers; focus goes to the menu.

Notes

  • A medium impact haptic plays when the menu opens, if the app provides an adapter through HapticsProvider.
  • The menu uses bg-surface-raised with a border that is stronger in dark mode, and the backdrop uses bg-scrim.
  • The preview stays within the top and bottom safe areas and leaves room for the measured height of the menu.
  • Animations respect the system reduce motion setting.
  • On iOS the content renders in a full-window overlay so it appears above native screens.