Pro component
Requires all-access to use the source code
npx expo install react-native-keyboard-controller
import * as Haptics from 'expo-haptics';
import * as React from 'react';
import { Image, Platform, Pressable, ScrollView, View } from 'react-native';
import Animated, { ZoomIn, ZoomOut } from 'react-native-reanimated';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/nativewindui/Avatar';
import { Button } from '@/components/nativewindui/Button';
import {
addOpacityToRgb,
Card,
CardBadge,
CardContent,
CardImage,
CardSubtitle,
CardTitle,
} from '@/components/nativewindui/Card';
import { ContextMenu } from '@/components/nativewindui/ContextMenu';
import { createContextItem, createContextSubMenu } from '@/components/nativewindui/ContextMenu/utils';
import { Icon } from '@/components/nativewindui/Icon';
import { IconProps } from '@/components/nativewindui/Icon/types';
import { Text } from '@/components/nativewindui/Text';
import { cn } from '@/lib/cn';
import { useColorScheme } from '@/lib/useColorScheme';
export default function Screen() {
return (
<ScrollView contentContainerClassName="gap-6 p-4 py-6">
<ProjectCard />
<MessageCard />
<OrderCard />
<View className="pb-safe" />
</ScrollView>
);
}
const IOS_PREVIEW_CONFIG = {
borderRadius: 16,
} as const;
const PROJECT_IMAGE_SOURCE = {
uri: 'https://images.unsplash.com/photo-1517048676732-d65bc937f952?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
};
function ProjectCard() {
const { colors } = useColorScheme();
const [isFavorite, setIsFavorite] = React.useState(false);
const projectContextMenuItems = React.useMemo(
() => [
createContextItem({ actionKey: 'open', title: 'Open' }),
createContextItem({
actionKey: 'mark-as-favorite',
title: isFavorite ? 'Unmark as Favorite' : 'Mark as Favorite',
icon: Platform.select({
ios: { name: isFavorite ? 'heart.fill' : 'heart' },
}),
state: Platform.select({ android: { checked: isFavorite } }),
keepOpenOnPress: true,
}),
createContextItem({ actionKey: 'rename', title: 'Rename' }),
createContextSubMenu(
{
title: 'Share this project',
iOSItemSize: 'small',
iOSType: 'inline',
},
[
createContextItem({
actionKey: 'link',
title: 'Link',
icon: { name: 'link' },
}),
createContextItem({
actionKey: 'email',
title: 'Email',
icon: { name: 'envelope' },
}),
createContextItem({
actionKey: 'message',
title: 'Message',
icon: { name: 'message' },
}),
]
),
],
[isFavorite]
);
return (
<ContextMenu
items={projectContextMenuItems}
auxiliaryPreviewPosition="center"
iosPreviewConfig={IOS_PREVIEW_CONFIG}
materialMinWidth={240}
iosRenderPreview={() => {
return <Image className="aspect-square w-screen" source={PROJECT_IMAGE_SOURCE} />;
}}
onItemPress={(item) => {
if (item.actionKey === 'mark-as-favorite') {
setIsFavorite((prev) => !prev);
}
}}>
<Pressable className="ios:rounded-2xl android:border-muted/70 border-muted rounded-xl border border-dashed">
<Card>
<CardImage
source={PROJECT_IMAGE_SOURCE}
materialRootClassName="absolute top-0 left-0 right-0 bottom-0 rounded-xl"
/>
<CardBadge>
<Text>Long press me</Text>
</CardBadge>
<CardContent
className="pt-48"
linearGradientColors={[
'transparent',
'transparent',
addOpacityToRgb(colors.card, 0.8),
colors.card,
]}>
<CardTitle>Project Proposal</CardTitle>
<CardSubtitle>Updated yesterday</CardSubtitle>
</CardContent>
</Card>
</Pressable>
</ContextMenu>
);
}
const EMOJI_TO_ICON: Record<string, IconProps> = {
love: {
name: 'heart.fill',
},
like: {
sfSymbol: { name: 'hand.thumbsup.fill' },
materialCommunityIcon: {
name: 'thumb-up',
},
},
dislike: {
sfSymbol: { name: 'hand.thumbsdown.fill' },
materialCommunityIcon: {
name: 'thumb-down',
},
},
exclamation: {
name: 'exclamationmark',
},
question: {
name: 'questionmark',
},
};
const AVATAR_SOURCE = { uri: 'https://avatars.githubusercontent.com/u/63797719?v=4' };
type SelectedEmoji = keyof typeof EMOJI_TO_ICON | null;
function MessageCard() {
const [selectedEmoji, setSelectedEmoji] = React.useState<SelectedEmoji>(null);
const [isPinned, setIsPinned] = React.useState(false);
const messageContextMenuItems = [
createContextItem({
actionKey: 'pin',
title: isPinned ? 'Unpin' : 'Pin',
icon: { name: isPinned ? 'pin.fill' : 'pin' },
keepOpenOnPress: true,
}),
createContextItem({
actionKey: 'forward',
title: 'Forward',
icon: { name: 'arrowshape.turn.up.right' },
}),
createContextItem({
actionKey: 'delete',
title: 'Delete',
destructive: true,
icon: { name: 'trash' },
}),
];
function handleEmojiPress(emoji: string) {
return () => {
if (emoji === selectedEmoji) {
setSelectedEmoji('');
return;
}
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setSelectedEmoji(emoji);
};
}
return (
<ContextMenu
items={messageContextMenuItems}
materialAlign="start"
auxiliaryPreviewPosition="center"
iosPreviewConfig={IOS_PREVIEW_CONFIG}
onItemPress={(item) => {
if (item.actionKey === 'pin') {
setIsPinned((prev) => !prev);
}
}}
renderAuxiliaryPreview={() => {
return (
<View
className={cn(
'bg-card flex-row gap-1 rounded-full p-0.5',
Platform.OS === 'ios' && 'bg-card/60 dark:bg-border/70'
)}>
<EmojiButton
onPress={handleEmojiPress('love')}
selectedEmoji={selectedEmoji}
name="love"
/>
<EmojiButton
onPress={handleEmojiPress('like')}
selectedEmoji={selectedEmoji}
name="like"
/>
<EmojiButton
onPress={handleEmojiPress('dislike')}
selectedEmoji={selectedEmoji}
name="dislike"
/>
<EmojiButton
onPress={handleEmojiPress('exclamation')}
selectedEmoji={selectedEmoji}
name="exclamation"
/>
<EmojiButton
onPress={handleEmojiPress('question')}
selectedEmoji={selectedEmoji}
name="question"
/>
</View>
);
}}>
<Pressable className="ios:rounded-2xl android:border-muted/70 border-muted rounded-xl border border-dashed">
<Card>
<CardBadge className="bg-muted/20 dark:bg-muted/30 android:opacity-70 -z-10">
<Text>Long press me</Text>
</CardBadge>
<CardContent className="ios:pt-10 pt-4">
<View className="flex-row items-center gap-3">
<Avatar alt="Zach Nugent's Avatar" className="h-16 w-16">
<AvatarImage source={AVATAR_SOURCE} />
<AvatarFallback>
<Text className="dark:android:text-background text-white">ZN</Text>
</AvatarFallback>
</Avatar>
<View>
<Text variant="caption2" className="text-muted-foreground px-1 pb-1">
Zach Nugent
</Text>
<View
className={cn(
'bg-background dark:bg-muted-foreground relative rounded-2xl px-3 py-1.5',
Platform.OS === 'ios' && 'dark:bg-muted'
)}>
<Text>Hey, got a minute?</Text>
{!selectedEmoji ? null : (
<Animated.View
key={selectedEmoji}
entering={ZoomIn.springify().damping(22).mass(0.9).stiffness(350)}
exiting={ZoomOut.duration(150)}
className={cn('bg-card absolute -top-4 rounded-full p-px', '-right-3')}>
<View className="bg-primary rounded-full p-1">
<Icon {...EMOJI_TO_ICON[selectedEmoji]} size={18} color="white" />
{Platform.OS === 'ios' && (
<>
<View
className={cn(
'bg-primary absolute bottom-0 h-2 w-2 rounded-full',
'right-0'
)}
/>
<View
className={cn(
'bg-primary absolute -bottom-1 h-1 w-1 rounded-full',
'-right-1'
)}
/>
</>
)}
</View>
</Animated.View>
)}
</View>
</View>
</View>
</CardContent>
</Card>
</Pressable>
</ContextMenu>
);
}
function EmojiButton({
onPress,
selectedEmoji,
name,
}: {
onPress: () => void;
selectedEmoji: SelectedEmoji;
name: string;
}) {
const { colors } = useColorScheme();
return (
<Button size="icon" variant="plain" onPress={onPress} className="ios:rounded-full rounded-full">
{selectedEmoji === name && (
<Animated.View
entering={ZoomIn.springify().damping(22).mass(0.9).stiffness(350)}
exiting={ZoomOut.duration(150)}
className="bg-primary absolute bottom-0 left-0 right-0 top-0 rounded-full"
/>
)}
<Icon {...EMOJI_TO_ICON[name]} color={selectedEmoji === name ? 'white' : colors.grey} />
</Button>
);
}
const ORDER_IMAGE_SOURCE = {
uri: 'https://images.unsplash.com/photo-1644079446600-219068676743?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
};
function OrderCard() {
const { colors } = useColorScheme();
const [isLoading, setIsLoading] = React.useState(true);
const [status, setStatus] = React.useState('pending');
const orderContextMenuItems = React.useMemo(
() => [
createContextItem({ actionKey: 'view-details', title: 'View Details' }),
createContextItem({ actionKey: 'edit-order', title: 'Edit Order' }),
createContextItem({ actionKey: 'cancel-order', title: 'Cancel Order', destructive: true }),
createContextSubMenu({ title: 'Change Status' }, [
createContextItem({
actionKey: 'pending',
title: 'Pending',
state: { checked: status === 'pending' },
keepOpenOnPress: Platform.OS === 'android',
}),
createContextItem({
actionKey: 'shipped',
title: 'Shipped',
state: { checked: status === 'shipped' },
}),
createContextItem({
actionKey: 'delivered',
title: 'Delivered',
state: { checked: status === 'delivered' },
}),
]),
createContextItem({
actionKey: 'download-invoice',
title: 'Download Invoice',
icon: { name: 'doc' },
loading: isLoading,
keepOpenOnPress: true,
}),
],
[isLoading, status]
);
return (
<ContextMenu
title="Order #8234"
items={orderContextMenuItems}
materialMinWidth={230}
materialAlign="start"
auxiliaryPreviewPosition="center"
iosPreviewConfig={IOS_PREVIEW_CONFIG}
onItemPress={(item) => {
if (item.actionKey === 'download-invoice') {
if (!isLoading) {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 1500);
}
}
if (item.actionKey === 'pending') {
setStatus('pending');
}
if (item.actionKey === 'shipped') {
setStatus('shipped');
}
if (item.actionKey === 'delivered') {
setStatus('delivered');
}
}}>
<Pressable
className="ios:rounded-2xl android:border-muted/70 border-muted rounded-xl border border-dashed"
onLongPress={() => {
if (isLoading) {
setTimeout(() => {
setIsLoading(false);
}, 1500);
}
}}>
<Card>
<CardImage
source={ORDER_IMAGE_SOURCE}
materialRootClassName="absolute top-0 left-0 right-0 bottom-0 rounded-xl"
/>
<CardBadge>
<Text>Long press me</Text>
</CardBadge>
<CardContent
className="pt-48"
linearGradientColors={[
'transparent',
'transparent',
addOpacityToRgb(colors.card, 0.8),
colors.card,
]}>
<CardTitle>Order #8234</CardTitle>
<CardSubtitle>Placed 2 days ago</CardSubtitle>
</CardContent>
</Card>
</Pressable>
</ContextMenu>
);
}
Unlock All Pro Templates & Components
Elevate your app with powerful, native-feeling components and templates. Build faster with beautiful ready-to-use designs.
The child component needs to accept Pressable props to ensure compatibility on Android.
import { ContextMenu } from '@/components/nativewindui/ContextMenu';
import {
createContextItem,
createContextSubMenu,
} from '@/components/nativewindui/ContextMenu/utils';
<ContextMenu
className="rounded-md"
items={[
createContextItem({
actionKey: 'first',
title: 'Item 1',
}),
createContextSubMenu({ title: 'Submenu 1', iOSItemSize: 'small' }, [
createContextItem({
actionKey: 'sub-first',
title: 'Sub Item 1',
}),
createContextItem({
actionKey: 'sub-second',
title: 'Sub Item 2',
}),
]),
]}
onItemPress={(item) => {
console.log('Item Pressed', item);
}}>
<Pressable>
<Text>Long Press Me</Text>
</Pressable>
</ContextMenu>
ContextMenu
Inherits all the props from React Native's View component.
Prop | Type | Default | Description |
---|---|---|---|
title | string | The title of the context menu. | |
items | (ContextItem | ContextSubMenu)[] | The items or submenus to display in the context menu. | |
iOSItemSize | 'small' | 'medium' | 'large' | The preferred item size on iOS. | |
children | React.ReactNode | The child component to render inside the context menu. It needs to accept Pressable props to ensure compatibility on Android | |
onItemPress | (item: Omit<ContextItem, 'icon'>) => void | Callback function triggered when an item is pressed. | |
enabled | boolean | true | Whether the context menu is enabled. |
iosRenderPreview | () => React.ReactElement | Function to render a preview on iOS. | |
iosOnPressMenuPreview | () => void | Callback for pressing the menu preview on iOS. | |
renderAuxiliaryPreview | () => React.ReactElement | Function to render an auxiliary preview. | |
auxiliaryPreviewPosition | 'start' | 'center' | 'end' | Position for the auxiliary preview. | |
materialPortalHost | string | The host for the Android portal. | |
materialSideOffset | number | 2 | Side offset for Android context menu. |
materialAlignOffset | number | Alignment offset for Android context menu. | |
materialAlign | 'start' | 'center' | 'end' | Alignment for Android context menu. | |
materialWidth | number | Width for the Android context menu. | |
materialMinWidth | number | Minimum width for the Android context menu. | |
materialLoadingText | string | Text to display while the context menu is loading. | |
materialSubMenuTitlePlaceholder | string | Placeholder title for the Android submenu. | |
materialOverlayClassName | string | Class name for the Android overlay. |
createContextSubMenu
: A utility function to create a context sub-menu.
createContextItem
: A utility function to create a context item.
import { createContextSubMenu, createContextItem } from '@/components/nativewindui/ContextMenu/utils';
const MENU = [
createContextSubMenu({ title: 'Submenu' }, [
createContextItem({ actionKey: '1', title: 'Item 1' }),
createContextItem({ actionKey: '2', title: 'Item 2' }),
]),
];