Skip to main content
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

title
string
required
The text displayed in the collapsible header
children
ReactNode
required
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>
  );
}

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

name
string
required
Icon name:
  • iOS: Any SF Symbol name (e.g., "house.fill", "chevron.right")
  • Android/Web: Automatically mapped to Material Icons
Available mappings:
  • house.fillhome
  • paperplane.fillsend
  • chevron.left.forwardslash.chevron.rightcode
  • chevron.rightchevron-right
size
number
default:"24"
Icon size in pixels
color
string | OpaqueColorValue
required
Icon tint color
weight
'ultraLight' | 'thin' | 'light' | 'regular' | 'medium' | 'semibold' | 'bold' | 'heavy' | 'black'
default:"'regular'"
SF Symbol weight (iOS only)
style
StyleProp<ViewStyle>
Additional style props

Usage

import { IconSymbol } from '@/components/ui/icon-symbol';

export default function MyScreen() {
  return (
    <IconSymbol
      name="house.fill"
      size={24}
      color="#000000"
    />
  );
}

Platform-Specific Implementation

The component uses platform-specific files:
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,
      ]}
    />
  );
}

ParallaxScrollView

A scroll view with a parallax header effect that scales and translates as the user scrolls.

Props

headerImage
ReactElement
required
The component to render in the parallax header (typically an Image or IconSymbol)
headerBackgroundColor
{ dark: string; light: string }
required
Background color for the header in both color schemes
children
ReactNode
required
The scrollable content

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>
  );
}

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>
  );
}
A link component that opens URLs in an in-app browser on native platforms.

Props

href
string
required
The URL to open
...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>
  );
}

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:
app/(tabs)/_layout.tsx
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

Use Collapsible to organize long content into scannable sections. Keep titles short and descriptive.
When using IconSymbol, add new icon mappings to the MAPPING object in components/ui/icon-symbol.tsx for Android/web support.
Use ParallaxScrollView on main screens where you want a polished, dynamic header. Keep header height at 250px for consistency.

Themed Components

Learn about ThemedText and ThemedView

Components Overview

View all components

Build docs developers (and LLMs) love