logoChisa UI

Command Palette

Search for a command to run...

Componentschevron_rightExpandableCard

ExpandableCard

Card with smooth expand/collapse animation, rotating arrow, and customizable content.

9:41
signal_cellular_altwifibattery_full
cabin

Nordic Cabin

Weekend getaway

keyboard_arrow_down

Experience serene mountain views and cozy interiors.

Wi-FiParking
$120/night

Preview Controls

Animation

Smooth expand/collapse with rotating arrow

This is a CSS-based preview of the animation. The actual React Native component uses physics-based springs for a more natural feel.

Installation

$

Usage

Example.tsx
import { View, Text, Image } from 'react-native';
import { ExpandableCard } from './components/ExpandableCard';
export default function App() {
return (
<View style={{ flex: 1, padding: 16 }}>
<ExpandableCard
title="Nordic Cabin"
subtitle="Weekend getaway in Oslo"
thumbnail={require('./assets/cabin.jpg')}
onToggle={(expanded) => console.log('Expanded:', expanded)}
>
<Text style={{ color: '#1A1A1A', marginBottom: 12 }}>
Experience the vibrant colors of fall in a serene mountain getaway.
</Text>
<View style={{ flexDirection: 'row', gap: 8 }}>
<Text style={{ backgroundColor: '#F5F5F5', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, fontSize: 12 }}>Wi-Fi</Text>
<Text style={{ backgroundColor: '#F5F5F5', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, fontSize: 12 }}>Parking</Text>
</View>
</ExpandableCard>
</View>
);
}

Full Component Code

ExpandableCard.tsx
1import React, { useState, useCallback } from 'react';
2import { View, Text, StyleSheet, TouchableOpacity, Image, ViewStyle, StyleProp, ImageSourcePropType } from 'react-native';
3import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing, interpolate, Extrapolation } from 'react-native-reanimated';
4
5interface ExpandableCardProps {
6 title: string;
7 subtitle?: string;
8 thumbnail?: ImageSourcePropType;
9 children: React.ReactNode;
10 duration?: number;
11 initialExpanded?: boolean;
12 onToggle?: (expanded: boolean) => void;
13 style?: StyleProp<ViewStyle>;
14}
15
16export const ExpandableCard: React.FC<ExpandableCardProps> = ({
17 title, subtitle, thumbnail, children,
18 duration = 200, initialExpanded = false, onToggle, style,
19}) => {
20 const [expanded, setExpanded] = useState(initialExpanded);
21 const [contentHeight, setContentHeight] = useState(0);
22 const progress = useSharedValue(initialExpanded ? 1 : 0);
23 const rotation = useSharedValue(initialExpanded ? 180 : 0);
24
25 const timingConfig = { duration, easing: Easing.bezier(0.25, 0.1, 0.25, 1) };
26
27 const handleToggle = useCallback(() => {
28 const newExpanded = !expanded;
29 setExpanded(newExpanded);
30 progress.value = withTiming(newExpanded ? 1 : 0, timingConfig);
31 rotation.value = withTiming(newExpanded ? 180 : 0, timingConfig);
32 onToggle?.(newExpanded);
33 }, [expanded, onToggle]);
34
35 const animatedContentStyle = useAnimatedStyle(() => ({
36 height: interpolate(progress.value, [0, 1], [0, contentHeight || 200], Extrapolation.CLAMP),
37 opacity: interpolate(progress.value, [0, 0.5, 1], [0, 0, 1]),
38 overflow: 'hidden',
39 }));
40
41 const animatedArrowStyle = useAnimatedStyle(() => ({
42 transform: [{ rotate: \`\${rotation.value}deg\` }],
43 }));
44
45 return (
46 <Animated.View style={[styles.container, style]}>
47 <TouchableOpacity onPress={handleToggle} activeOpacity={0.9}>
48 <View style={styles.header}>
49 {thumbnail && <Image source={thumbnail} style={styles.thumbnail} />}
50 <View style={styles.textContainer}>
51 <Text style={styles.title}>{title}</Text>
52 {subtitle && <Text style={styles.subtitle}>{subtitle}</Text>}
53 </View>
54 <Animated.View style={[styles.toggleButton, { backgroundColor: expanded ? '#E16C2D' : 'rgba(225,108,45,0.1)' }]}>
55 <Animated.Text style={[styles.arrow, animatedArrowStyle, { color: expanded ? '#FFF' : '#E16C2D' }]}></Animated.Text>
56 </Animated.View>
57 </View>
58 </TouchableOpacity>
59 <Animated.View style={animatedContentStyle}>
60 <View onLayout={(e) => setContentHeight(e.nativeEvent.layout.height)}>
61 <View style={styles.divider} />
62 <View style={styles.content}>{children}</View>
63 </View>
64 </Animated.View>
65 </Animated.View>
66 );
67};
68
69const styles = StyleSheet.create({
70 container: { backgroundColor: '#FFF', borderRadius: 12, borderWidth: 1, borderColor: '#E5E5E5', overflow: 'hidden' },
71 header: { flexDirection: 'row', alignItems: 'center', padding: 16, gap: 12 },
72 thumbnail: { width: 56, height: 56, borderRadius: 8 },
73 textContainer: { flex: 1 },
74 title: { fontSize: 16, fontWeight: '700', color: '#1A1A1A' },
75 subtitle: { fontSize: 14, color: '#6B6B6B' },
76 toggleButton: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
77 arrow: { fontSize: 12 },
78 divider: { height: 1, backgroundColor: '#E5E5E5' },
79 content: { padding: 16 },
80});

Props

NameTypeDefaultDescription
titlestring-Card title
subtitlestring-Card subtitle
thumbnailImageSourcePropType-Thumbnail image source
childrenReact.ReactNode-Expanded content
durationnumber200Animation duration in ms
initialExpandedbooleanfalseInitial expanded state
onToggle(expanded: boolean) => void-Callback when expanded state changes