Hover Dropdown

Here is an example of a Web-only dropdown, triggered by hovered interactions.

You can preview this example on Expo Snack.

import React from 'react'
import {
StyleSheet,
SafeAreaView,
View,
Text,
ViewProps,
Platform,
} from 'react-native'
import {
MotiPressable,
useMotiPressable,
useMotiPressableAnimatedProps,
} from 'moti/interactions'
import { MotiView } from 'moti'
import { Ionicons } from '@expo/vector-icons'
function MenuItemBg() {
const state = useMotiPressable(
'item',
({ hovered, pressed }) => {
'worklet'
return {
opacity: hovered || pressed ? 0.4 : 0,
}
},
[]
)
return <MotiView state={state} style={styles.itemBg} />
}
function MenuItemArrow() {
const state = useMotiPressable(
'item',
({ hovered, pressed }) => {
'worklet'
return {
opacity: hovered || pressed ? 1 : 0,
translateX: hovered || pressed ? 0 : -10,
}
},
[]
)
return (
<MotiView
transition={{ type: 'timing' }}
style={styles.itemArrow}
state={state}
>
<Ionicons name="ios-arrow-forward" size={18} color="white" />
</MotiView>
)
}
function MenuItem({
title,
description,
color,
icon,
}: {
title: string
description: string
color: string
icon: React.ComponentProps<typeof Ionicons>['name']
}) {
return (
<MotiPressable onPress={console.log} style={styles.item} id="item">
<MenuItemBg />
<View style={[styles.iconContainer, { backgroundColor: color }]}>
<Ionicons size={32} color="black" name={icon} />
</View>
<View style={styles.itemContent}>
<View style={styles.titleContainer}>
<Text style={[styles.text, styles.title]}>{title}</Text>
<MenuItemArrow />
</View>
<Text style={[styles.text, styles.subtitle]}>{description}</Text>
</View>
</MotiPressable>
)
}
function Dropdown() {
const dropdownState = useMotiPressable(
'menu',
({ hovered, pressed }) => {
'worklet'
return {
opacity: pressed || hovered ? 1 : 0,
translateY: pressed || hovered ? 0 : -5,
}
},
[]
)
const animatedProps = useMotiPressableAnimatedProps<ViewProps>(
'menu',
({ hovered, pressed }) => {
'worklet'
console.log('hovered', hovered)
return {
pointerEvents: pressed || hovered ? 'auto' : 'none',
}
},
[]
)
return (
<MotiView
style={styles.dropdown}
animatedProps={animatedProps}
transition={{ type: 'timing' }}
>
<MotiView
style={[styles.dropdownContent, shadow]}
transition={{ type: 'timing', delay: 20 }}
state={dropdownState}
>
<Text style={[styles.text, styles.heading]}>BeatGig Products</Text>
<MenuItem
title="Colleges"
description="For Greek organizations & university program boards"
color="#FFF500"
icon="school-outline"
/>
<MenuItem
title="Venues"
description="For bars, nightclubs, restaurants, country clubs, & vineyards"
color="#50E3C2"
icon="business-outline"
/>
<MenuItem
title="Artists"
description="For artists, managers & agents"
color="#FF0080"
icon="mic-outline"
/>
</MotiView>
</MotiView>
)
}
function TriggerBg() {
const state = useMotiPressable(
'trigger',
({ hovered, pressed }) => {
'worklet'
return {
opacity: hovered || pressed ? 0.2 : 0,
}
},
[]
)
return <MotiView state={state} style={styles.triggerBg} />
}
function Trigger() {
return (
<MotiPressable id="trigger">
<TriggerBg />
<View style={styles.triggerContainer}>
<Text style={[styles.text, styles.trigger]}>Our Products</Text>
<Ionicons
name="chevron-down"
style={styles.chevron}
color="white"
size={20}
/>
</View>
</MotiPressable>
)
}
function Menu() {
return (
<MotiPressable id="menu">
<Trigger />
<Dropdown />
</MotiPressable>
)
}
export default function MotiPressableMenu() {
return (
<SafeAreaView style={styles.container}>
<View style={styles.wrapper}>
<Menu />
</View>
</SafeAreaView>
)
}
const shadow = Platform.select({
web: {
boxShadow: `rgb(255 255 255 / 10%) 0px 50px 100px -20px, rgb(255 255 255 / 50%) 0px 30px 60px -30px`,
},
}) as any
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'black',
},
wrapper: {
padding: 32,
alignItems: 'flex-start',
},
text: {
color: 'white',
fontFamily: Platform.OS === 'web' ? 'SF Pro Rounded' : undefined,
fontSize: 14,
},
dropdown: {
position: 'absolute',
top: '100%',
width: 500,
paddingTop: 4,
},
dropdownContent: {
backgroundColor: 'black',
paddingHorizontal: 16,
borderRadius: 8,
paddingVertical: 32,
},
trigger: {
fontSize: 16,
fontWeight: 'bold',
alignItems: 'center',
...Platform.select({
web: { cursor: 'pointer' },
}),
},
triggerBg: {
backgroundColor: 'white',
borderRadius: 4,
...StyleSheet.absoluteFillObject,
},
heading: {
textTransform: 'uppercase',
fontWeight: 'bold',
color: '#888888',
marginLeft: 16,
fontSize: 16,
},
item: {
padding: 16,
borderRadius: 8,
flexDirection: 'row',
marginTop: 8,
...Platform.select({
web: { cursor: 'pointer' },
}),
},
itemBg: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#333',
},
iconContainer: {
height: 50,
width: 50,
borderRadius: 25,
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
title: {
fontSize: 18,
fontWeight: '600',
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: '#888888',
fontWeight: '500',
},
itemContent: {
flex: 1,
},
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
},
itemArrow: {
marginLeft: 4,
},
chevron: {
marginTop: 1,
marginLeft: 8,
},
triggerContainer: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 12,
marginVertical: 8,
},
})