Welcome to Day 4 of the React Native Advent Calendar!
Today we’re diving into something truly exciting: Expo UI - a package that gives React Native developers the power to drop down to truly native SwiftUI and Jetpack Compose components. This isn’t another UI library or cross-platform abstraction - it’s direct access to native primitives!
What is Expo UI?
Expo UI brings native SwiftUI (iOS) and Jetpack Compose (Android) primitives directly to React Native applications. Unlike traditional UI libraries that create abstractions or reimplementations of native components, Expo UI provides a 1-to-1 mapping to actual native views.
Why It Matters
React Native is fantastic for building cross-platform apps, but sometimes you need access to platform-specific native components that haven’t been bridged yet, or you want that truly native feel that only SwiftUI or Jetpack Compose can provide.
With Expo UI, you can:
- ✅ Use the latest SwiftUI and Jetpack Compose components directly
- ✅ Get native performance and behavior out of the box
- ✅ Mix React Native, Expo UI, and custom components seamlessly
Important concept: This is not a cross-platform abstraction. You’ll write platform-specific files (.ios.tsx and .android.tsx) using the appropriate Expo UI package for each platform. This gives you maximum power and flexibility!
SwiftUI Components
Expo UI for SwiftUI is in beta and covers the most common SwiftUI components. Let’s explore what you can build with it.
📖 SwiftUI Components Reference
The Host Component
Every SwiftUI component must be wrapped in a Host component. Think of it like an SVG or Canvas element - it creates a boundary between React Native and SwiftUI contexts.
import { CircularProgress, Host } from '@expo/ui/swift-ui';
export default function LoadingView() {
return (
<Host matchContents>
<CircularProgress />
</Host>
);
}Available SwiftUI Components
Expo UI provides access to a comprehensive set of native iOS components:
Input Controls
- Button - Native buttons with variants (default, borderless)
- TextField - Single-line text input with autocorrection
- Switch - Toggle controls and checkboxes
- Slider - Adjustable value controls
Progress Indicators
- LinearProgress - Horizontal progress bars
- CircularProgress - Radial progress indicators
- Gauge - Advanced value indicators with gradients
Selection Components
- Picker - Segmented and wheel pickers
- DateTimePicker - Native date and time selection
- ColorPicker - Color selection interface
Containers
- List - Native scrollable lists with edit modes
- BottomSheet - Modal panels from bottom
- ContextMenu - Dropdown menus
Practical Example: Native Button
import { Button, Host, VStack, Text } from '@expo/ui/swift-ui';
import { useState } from 'react';
export default function ProfileEdit() {
const [isEditing, setIsEditing] = useState(false);
return (
<Host style={{ flex: 1 }}>
<VStack spacing={16}>
<Text>Profile Settings</Text>
<Button variant="default" onPress={() => setIsEditing(true)}>
Edit Profile
</Button>
<Button variant="borderless" onPress={() => setIsEditing(false)}>
Cancel
</Button>
</VStack>
</Host>
);
}Layout with HStack and VStack
Inside SwiftUI contexts, you don’t use Flexbox. Instead, use HStack (horizontal) and VStack (vertical) for layout:
import { HStack, VStack, Button, Text, Host } from '@expo/ui/swift-ui';
export default function ActionBar() {
return (
<Host matchContents>
<VStack spacing={12}>
<Text>Choose an action</Text>
<HStack spacing={8}>
<Button onPress={() => console.log('Save')}>Save</Button>
<Button variant="borderless" onPress={() => console.log('Cancel')}>
Cancel
</Button>
</HStack>
</VStack>
</Host>
);
}Pro tip: The matchContents prop on Host makes it size to fit its SwiftUI children, perfect for inline components!
Jetpack Compose Components (Alpha)
Android support via Jetpack Compose is currently in alpha and actively being worked on. It’s not available in Expo Go and requires a development build.
📖 Jetpack Compose Components Reference
Available Jetpack Compose Components
The @expo/ui/jetpack-compose package currently provides 14 native Android components:
Input Controls
- Button - Material Design buttons
- TextInput - Single and multi-line text input
- Switch - Toggle and checkbox variants
- Slider - Value adjustment controls
- Picker - Radio and segmented pickers
Feedback
- CircularProgress - Circular loading indicators
- LinearProgress - Linear progress bars
Selection
- Chip - Material chips (assist, filter, input, suggestion variants)
Navigation
- ContextMenu - Dropdown menus
Date/Time
- DateTimePicker - Native date and time selection
Practical Example: Android Button
import { Button, Host } from '@expo/ui/jetpack-compose';
import { useState } from 'react';
export default function ProfileEdit() {
const [isEditing, setIsEditing] = useState(false);
return (
<Host style={{ flex: 1 }}>
<Button variant="default" onPress={() => setIsEditing(true)}>
Edit Profile
</Button>
</Host>
);
}Note: Jetpack Compose support is evolving rapidly. Check the official documentation for the latest updates and API changes.
Platform-Specific Files: The Right Way
The beauty of Expo UI is embracing platform differences rather than hiding them. Here’s how to structure your components:
First, create a shared interface for the button props:
// NativeButton.tsx - TypeScript types and shared logic
export interface NativeButtonProps {
title: string;
onPress: () => void;
variant?: 'default' | 'borderless';
}Then, create the platform-specific implementations:
// NativeButton.ios.tsx - iOS implementation with SwiftUI
import { Button, Host } from '@expo/ui/swift-ui';
import { NativeButtonProps } from './NativeButton';
export default function NativeButton({ title, onPress, variant = 'default' }: NativeButtonProps) {
return (
<Host matchContents>
<Button variant={variant} onPress={onPress}>
{title}
</Button>
</Host>
);
}Same for Android:
// NativeButton.android.tsx - Android implementation with Jetpack Compose
import { Button, Host } from '@expo/ui/jetpack-compose';
import { NativeButtonProps } from './NativeButton';
export default function NativeButton({ title, onPress, variant = 'default' }: NativeButtonProps) {
return (
<Host matchContents>
<Button variant={variant} onPress={onPress}>
{title}
</Button>
</Host>
);
}Now you can import NativeButton anywhere, and React Native will automatically load the correct platform-specific implementation!
import NativeButton from '@/components/NativeButton';
export default function ProfileScreen() {
return (
<View>
{/* Automatically uses .ios.tsx on iOS and .android.tsx on Android */}
<NativeButton title="Edit Profile" onPress={() => console.log('Edit')} />
</View>
);
}Getting Started
Installation
# Install Expo UI
npx expo install @expo/ui
# Create a development build (required for Jetpack Compose)
npx expo run:ios
npx expo run:androidYour First SwiftUI Component
import { Host, Text, VStack, Button } from '@expo/ui/swift-ui';
export default function HelloNative() {
return (
<Host style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<VStack spacing={20}>
<Text>Hello from SwiftUI!</Text>
<Button onPress={() => alert('Native button pressed!')}>Press Me</Button>
</VStack>
</Host>
);
}Important Notes
- Flexbox only applies to Host: Inside SwiftUI contexts, use
HStackandVStackfor layout - Full TypeScript support: Use TypeScript to explore available props and components
- Modifiers: Import modifiers from
@expo/ui/swift-ui/modifiersfor advanced styling - Not in Expo Go: Jetpack Compose requires development builds
When to Use Expo UI
Great use cases:
- ✅ You need a specific native component not available in React Native
- ✅ You want truly native platform look and feel
- ✅ You’re building platform-specific features
- ✅ You need access to the latest iOS/Android UI components
- ✅ Performance is critical and you need native rendering
Maybe not ideal for:
- ❌ Simple cross-platform UI that React Native handles well
- ❌ You want to write code once for all platforms
- ❌ Your team doesn’t have iOS/Android native experience
Wrapping Up
That’s it for Day 4 of the React Native Advent Calendar!
Expo UI is a game-changer for React Native developers who want to drop down to truly native components without writing native modules. Whether you need SwiftUI’s latest components or Jetpack Compose’s Material Design, you now have direct access!
Key Takeaways
- Expo UI provides 1-to-1 mappings to native SwiftUI and Jetpack Compose components
- Platform-specific files let you embrace platform differences rather than abstract them
- SwiftUI is in beta, Jetpack Compose is in alpha but rapidly evolving
- Mix and match React Native, Expo UI, and custom components freely
What to explore next:
- Browse the SwiftUI components
- Check out Jetpack Compose components
- Read the Expo UI guide
What native component are you most excited to use? Let me know on Twitter!
Tomorrow we’ll explore Day 5’s topic - see you then! 🎄