GymApp includes a collection of specialized UI components that handle common patterns like collapsible sections, icons, animations, and external links.
Collapsible
An expandable/collapsible content section with animated chevron icon.
Props
The text displayed in the collapsible header
The content to show/hide when the collapsible is toggled
Usage
import { Collapsible } from '@/components/ui/collapsible' ;
import { ThemedText } from '@/components/themed-text' ;
export default function MyScreen () {
return (
< Collapsible title = "File-based routing" >
< ThemedText >
This app has two screens: { ' ' }
< ThemedText type = "defaultSemiBold" > app/(tabs)/index.tsx </ ThemedText > and { ' ' }
< ThemedText type = "defaultSemiBold" > app/(tabs)/explore.tsx </ ThemedText >
</ ThemedText >
</ Collapsible >
);
}
From app/(tabs)/explore.tsx: < Collapsible title = "Images" >
< ThemedText >
For static images, you can use the < ThemedText type = "defaultSemiBold" > @2x </ ThemedText > and { ' ' }
< ThemedText type = "defaultSemiBold" > @3x </ ThemedText > suffixes to provide files for
different screen densities
</ ThemedText >
< Image
source = { require ( '@/assets/images/react-logo.png' ) }
style = { { width: 100 , height: 100 , alignSelf: 'center' } }
/>
< ExternalLink href = "https://reactnative.dev/docs/images" >
< ThemedText type = "link" > Learn more </ ThemedText >
</ ExternalLink >
</ Collapsible >
Implementation
The component uses internal state to track open/closed status and animates a chevron icon:
components/ui/collapsible.tsx
export function Collapsible ({ children , title } : PropsWithChildren & { title : string }) {
const [ isOpen , setIsOpen ] = useState ( false );
const theme = useColorScheme () ?? 'light' ;
return (
< ThemedView >
< TouchableOpacity
style = { styles . heading }
onPress = { () => setIsOpen (( value ) => ! value ) }
activeOpacity = { 0.8 } >
< IconSymbol
name = "chevron.right"
size = { 18 }
weight = "medium"
color = { theme === 'light' ? Colors . light . icon : Colors . dark . icon }
style = { { transform: [{ rotate: isOpen ? '90deg' : '0deg' }] } }
/>
< ThemedText type = "defaultSemiBold" > { title } </ ThemedText >
</ TouchableOpacity >
{ isOpen && < ThemedView style = { styles . content } > { children } </ ThemedView > }
</ ThemedView >
);
}
IconSymbol
Cross-platform icon component that uses native SF Symbols on iOS and Material Icons on Android/web.
Props
Icon name:
iOS: Any SF Symbol name (e.g., "house.fill", "chevron.right")
Android/Web: Automatically mapped to Material Icons
Available mappings:
house.fill → home
paperplane.fill → send
chevron.left.forwardslash.chevron.right → code
chevron.right → chevron-right
color
string | OpaqueColorValue
required
Icon tint color
weight
'ultraLight' | 'thin' | 'light' | 'regular' | 'medium' | 'semibold' | 'bold' | 'heavy' | 'black'
default: "'regular'"
SF Symbol weight (iOS only)
Usage
Basic Usage
With Weight (iOS)
Real Example
import { IconSymbol } from '@/components/ui/icon-symbol' ;
export default function MyScreen () {
return (
< IconSymbol
name = "house.fill"
size = { 24 }
color = "#000000"
/>
);
}
import { IconSymbol } from '@/components/ui/icon-symbol' ;
export default function MyScreen () {
return (
< IconSymbol
name = "chevron.right"
size = { 18 }
weight = "medium"
color = "#666666"
/>
);
}
From app/(tabs)/explore.tsx: < ParallaxScrollView
headerBackgroundColor = { { light: '#D0D0D0' , dark: '#353636' } }
headerImage = {
< IconSymbol
size = { 310 }
color = "#808080"
name = "chevron.left.forwardslash.chevron.right"
style = { styles . headerImage }
/>
} >
{ /* Content */ }
</ ParallaxScrollView >
The component uses platform-specific files:
components/ui/icon-symbol.ios.tsx
components/ui/icon-symbol.tsx
export function IconSymbol ({
name ,
size = 24 ,
color ,
style ,
weight = 'regular' ,
} : {
name : SymbolViewProps [ 'name' ];
size ?: number ;
color : string ;
style ?: StyleProp < ViewStyle >;
weight ?: SymbolWeight ;
}) {
return (
< SymbolView
weight = { weight }
tintColor = { color }
resizeMode = "scaleAspectFit"
name = { name }
style = { [
{
width: size ,
height: size ,
},
style ,
] }
/>
);
}
A scroll view with a parallax header effect that scales and translates as the user scrolls.
Props
The component to render in the parallax header (typically an Image or IconSymbol)
Background color for the header in both color schemes
Usage
From app/(tabs)/index.tsx: import { Image } from 'expo-image' ;
import ParallaxScrollView from '@/components/parallax-scroll-view' ;
export default function HomeScreen () {
return (
< ParallaxScrollView
headerBackgroundColor = { { light: '#A1CEDC' , dark: '#1D3D47' } }
headerImage = {
< Image
source = { require ( '@/assets/images/partial-react-logo.png' ) }
style = { styles . reactLogo }
/>
} >
{ /* Your content */ }
</ ParallaxScrollView >
);
}
From app/(tabs)/explore.tsx: import ParallaxScrollView from '@/components/parallax-scroll-view' ;
import { IconSymbol } from '@/components/ui/icon-symbol' ;
export default function TabTwoScreen () {
return (
< ParallaxScrollView
headerBackgroundColor = { { light: '#D0D0D0' , dark: '#353636' } }
headerImage = {
< IconSymbol
size = { 310 }
color = "#808080"
name = "chevron.left.forwardslash.chevron.right"
style = { styles . headerImage }
/>
} >
{ /* Your content */ }
</ ParallaxScrollView >
);
}
Implementation
Uses react-native-reanimated for smooth animations:
components/parallax-scroll-view.tsx
const HEADER_HEIGHT = 250 ;
export default function ParallaxScrollView ({
children ,
headerImage ,
headerBackgroundColor ,
} : Props ) {
const backgroundColor = useThemeColor ({}, 'background' );
const colorScheme = useColorScheme () ?? 'light' ;
const scrollRef = useAnimatedRef < Animated . ScrollView >();
const scrollOffset = useScrollOffset ( scrollRef );
const headerAnimatedStyle = useAnimatedStyle (() => {
return {
transform: [
{
translateY: interpolate (
scrollOffset . value ,
[ - HEADER_HEIGHT , 0 , HEADER_HEIGHT ],
[ - HEADER_HEIGHT / 2 , 0 , HEADER_HEIGHT * 0.75 ]
),
},
{
scale: interpolate ( scrollOffset . value , [ - HEADER_HEIGHT , 0 , HEADER_HEIGHT ], [ 2 , 1 , 1 ]),
},
],
};
});
return (
< Animated.ScrollView ref = { scrollRef } style = { { backgroundColor , flex: 1 } } scrollEventThrottle = { 16 } >
< Animated.View style = { [ styles . header , { backgroundColor: headerBackgroundColor [ colorScheme ] }, headerAnimatedStyle ] } >
{ headerImage }
</ Animated.View >
< ThemedView style = { styles . content } > { children } </ ThemedView >
</ Animated.ScrollView >
);
}
HelloWave
An animated waving hand emoji component.
Usage
import { HelloWave } from '@/components/hello-wave' ;
import { ThemedText } from '@/components/themed-text' ;
import { ThemedView } from '@/components/themed-view' ;
export default function HomeScreen () {
return (
< ThemedView style = { { flexDirection: 'row' , alignItems: 'center' , gap: 8 } } >
< ThemedText type = "title" > Welcome! </ ThemedText >
< HelloWave />
</ ThemedView >
);
}
Implementation
Uses react-native-reanimated for the waving animation:
components/hello-wave.tsx
export function HelloWave () {
return (
< Animated.Text
style = { {
fontSize: 28 ,
lineHeight: 32 ,
marginTop: - 6 ,
animationName: {
'50%' : { transform: [{ rotate: '25deg' }] },
},
animationIterationCount: 4 ,
animationDuration: '300ms' ,
} } >
👋
</ Animated.Text >
);
}
ExternalLink
A link component that opens URLs in an in-app browser on native platforms.
Props
...rest
ComponentProps<typeof Link>
All props from expo-router’s Link component (except href)
Usage
import { ExternalLink } from '@/components/external-link' ;
import { ThemedText } from '@/components/themed-text' ;
export default function MyScreen () {
return (
< ExternalLink href = "https://docs.expo.dev/router/introduction" >
< ThemedText type = "link" > Learn more </ ThemedText >
</ ExternalLink >
);
}
From app/(tabs)/explore.tsx: < Collapsible title = "File-based routing" >
< ThemedText >
This app has two screens: { ' ' }
< ThemedText type = "defaultSemiBold" > app/(tabs)/index.tsx </ ThemedText > and { ' ' }
< ThemedText type = "defaultSemiBold" > app/(tabs)/explore.tsx </ ThemedText >
</ ThemedText >
< ThemedText >
The layout file in < ThemedText type = "defaultSemiBold" > app/(tabs)/_layout.tsx </ ThemedText > { ' ' }
sets up the tab navigator.
</ ThemedText >
< ExternalLink href = "https://docs.expo.dev/router/introduction" >
< ThemedText type = "link" > Learn more </ ThemedText >
</ ExternalLink >
</ Collapsible >
Implementation
Opens links in an in-app browser on native platforms:
components/external-link.tsx
export function ExternalLink ({ href , ... rest } : Props ) {
return (
< Link
target = "_blank"
{ ... rest }
href = { href }
onPress = {async ( event ) => {
if ( process . env . EXPO_OS !== 'web' ) {
// Prevent the default behavior of linking to the default browser on native.
event . preventDefault ();
// Open the link in an in-app browser.
await openBrowserAsync ( href , {
presentationStyle: WebBrowserPresentationStyle . AUTOMATIC ,
});
}
} }
/>
);
}
HapticTab
A tab bar button component that provides haptic feedback on iOS when pressed.
Props
props
BottomTabBarButtonProps
required
All props from React Navigation’s BottomTabBarButtonProps
Usage
Typically used in tab navigator configuration:
import { Tabs } from 'expo-router' ;
import { HapticTab } from '@/components/haptic-tab' ;
export default function TabLayout () {
return (
< Tabs
screenOptions = { {
tabBarButton: HapticTab ,
} } >
< Tabs.Screen name = "index" />
< Tabs.Screen name = "explore" />
</ Tabs >
);
}
Implementation
Provides light haptic feedback on iOS:
components/haptic-tab.tsx
export function HapticTab ( props : BottomTabBarButtonProps ) {
return (
< PlatformPressable
{ ... props }
onPressIn = { ( ev ) => {
if ( process . env . EXPO_OS === 'ios' ) {
// Add a soft haptic feedback when pressing down on the tabs.
Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light );
}
props . onPressIn ?.( ev );
} }
/>
);
}
Best Practices
Collapsible sections for long content
Use Collapsible to organize long content into scannable sections. Keep titles short and descriptive.
Add icon mappings as needed
When using IconSymbol, add new icon mappings to the MAPPING object in components/ui/icon-symbol.tsx for Android/web support.
ParallaxScrollView for visual impact
Use ParallaxScrollView on main screens where you want a polished, dynamic header. Keep header height at 250px for consistency.
ExternalLink for all external URLs
Always use ExternalLink instead of plain Link for external URLs to provide a better native experience.
Themed Components Learn about ThemedText and ThemedView
Components Overview View all components