📅 Day 4
💡 tip

Expo UI: SwiftUI & Jetpack Compose in React Native

Drop down to truly native components with SwiftUI and Jetpack Compose using Expo UI

ExpoSwiftUIJetpack Compose

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.

📖 Expo UI Documentation

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.

app/components/Loading.ios.tsx
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

app/components/ProfileEdit.ios.tsx
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:

app/components/ActionBar.ios.tsx
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)
  • ContextMenu - Dropdown menus

Date/Time

  • DateTimePicker - Native date and time selection

Practical Example: Android Button

app/components/ProfileEdit.android.tsx
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:

app/components/NativeButton.tsx
// NativeButton.tsx - TypeScript types and shared logic
export interface NativeButtonProps {
	title: string;
	onPress: () => void;
	variant?: 'default' | 'borderless';
}

Then, create the platform-specific implementations:

app/components/NativeButton.ios.tsx
// 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:

app/components/NativeButton.android.tsx
// 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!

app/profile.tsx
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

terminal
# Install Expo UI
npx expo install @expo/ui

# Create a development build (required for Jetpack Compose)
npx expo run:ios
npx expo run:android

Your First SwiftUI Component

app/components/HelloNative.ios.tsx
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

  1. Flexbox only applies to Host: Inside SwiftUI contexts, use HStack and VStack for layout
  2. Full TypeScript support: Use TypeScript to explore available props and components
  3. Modifiers: Import modifiers from @expo/ui/swift-ui/modifiers for advanced styling
  4. 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

  1. Expo UI provides 1-to-1 mappings to native SwiftUI and Jetpack Compose components
  2. Platform-specific files let you embrace platform differences rather than abstract them
  3. SwiftUI is in beta, Jetpack Compose is in alpha but rapidly evolving
  4. Mix and match React Native, Expo UI, and custom components freely

What to explore next:

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! 🎄