Widgets are one of the best retention features in mobile. A habit tracker on the home screen, a delivery ETA in the Dynamic Island, a daily quote on the lock screen: each one gives users a reason to come back without opening your app. For years, adding widgets to a React Native app meant writing Swift in Xcode. expo-widgets changes that. You write widgets in React with Expo UI components, and a config plugin generates the native extension.
What you can build
- iOS home screen widgets in small, medium, and large sizes
- iOS Live Activities on the lock screen and in the Dynamic Island
- Android home screen widgets (opt-in, new in Expo SDK 58). Widgets run their own JavaScript bundle on a dedicated Hermes runtime.
Install
npx expo install expo-widgets @expo/ui
Widgets need a native extension, so they don't work in Expo Go. Use a development build.
1. Register the widget in app.json
{
"expo": {
"plugins": [
[
"expo-widgets",
{
"widgets": [
{
"name": "StreakWidget",
"displayName": "Streak",
"description": "See your current streak at a glance",
"ios": { "supportedFamilies": ["systemSmall", "systemMedium"] }
}
]
}
]
]
}
}
name must match the name you pass to createWidget. displayName and description appear in the system widget gallery.
2. Write the widget in React
Widget layouts use Expo UI components (SwiftUI under the hood on iOS). The component must start with the 'widget' directive:
// widgets/StreakWidget.tsx
import { Text, VStack } from "@expo/ui/swift-ui"
import { createWidget, type WidgetEnvironment } from "expo-widgets"
type StreakProps = { days: number; doneToday: boolean }
const StreakWidget = (props: StreakProps, environment: WidgetEnvironment) => {
"widget"
return (
<VStack>
<Text>🔥 {props.days} day streak</Text>
<Text>{props.doneToday ? "Done for today" : "Not done yet"}</Text>
</VStack>
)
}
export default createWidget("StreakWidget", StreakWidget, { days: 0, doneToday: false })
The third argument is the default props shown before your app sends any data.
3. Update the widget from your app
Whenever data changes, push new props. updateSnapshot updates immediately:
import StreakWidget from "@/widgets/StreakWidget"
export function onHabitCompleted(streak: number) {
StreakWidget.updateSnapshot({ days: streak, doneToday: true })
}
updateTimeline schedules future states, so the widget changes even when the app isn't running:
const midnight = new Date()
midnight.setHours(24, 0, 0, 0)
StreakWidget.updateTimeline([
{ date: new Date(), props: { days: 12, doneToday: true } },
{ date: midnight, props: { days: 12, doneToday: false } },
])
Timelines are perfect for countdowns, daily content, and "reset at midnight" states.
4. Live Activities
Live Activities show real-time progress on the lock screen and Dynamic Island. Define one with createLiveActivity. Live Activities don't get a widgets[] entry in app.json:
// widgets/DeliveryActivity.tsx
import { Text, VStack } from "@expo/ui/swift-ui"
import { createLiveActivity } from "expo-widgets"
type DeliveryProps = { etaMinutes: number; status: string }
const DeliveryLayout = (props: DeliveryProps) => {
"widget"
return (
<VStack>
<Text>{props.status}</Text>
<Text>{props.etaMinutes} min</Text>
</VStack>
)
}
export default createLiveActivity("DeliveryActivity", DeliveryLayout)
Start and update it from your app:
import DeliveryActivity from "@/widgets/DeliveryActivity"
const activity = DeliveryActivity.start(
{ etaMinutes: 15, status: "Your order is on the way" },
"myapp://orders/12345", // opens when the user taps the activity
)
await activity.update({ etaMinutes: 2, status: "Arriving now" })
The URL is a deep link into your app. See our Expo Router deep linking guide to route it to the right screen.
Android widgets
Android widgets are opt-in: enable Android in the plugin config (enableAndroid) and add the android options to each widget entry. They run in their own Hermes runtime, separate from your app's JavaScript, so keep widget code small and self-contained. Live Activities stay iOS-only.
Widget design tips
- One job per widget. Show the single number or action that matters.
- Design for glanceability. Large text, high contrast, and no dense lists.
- Deep link every tap to the relevant screen, not the home screen.
- Update on meaningful events, not on a timer. iOS budgets widget reloads.
- Support dark mode and tinted home screens. Use system colors where possible.
Ideas that work well as widgets
Habit and streak trackers (pair with our Scribble Calendar), to-do counts, countdowns, fitness rings, delivery and ride tracking, and "word of the day" style content. For in-app celebration when a streak is hit, add Expo Confetti and a success haptic.