Expo Widgets
Set up Expo Widgets for home-screen widgets in an Expo app with ReadyNative: iOS home-screen widget + Live Activity (Lock Screen, Dynamic Island) in…
Expo Go: no - dev build required
Ships native code. Make a dev build with bunx expo run:ios / bunx expo run:android (or bunx eas-cli build --profile development).
This gives you an iOS home-screen widget and a Live Activity (Lock Screen and Dynamic Island) through expo-widgets. The widget's layout is src/widgets/app-widget.tsx: a React component marked with the 'widget' directive and built from @expo/ui/swift-ui views, which Expo compiles into a real WidgetKit extension. The app feeds it through src/lib/widgets.ts, which implements the shim - widgets.update({ title, value, subtitle, symbol }) hands a new snapshot to the widget, and does nothing in Expo Go, on Android and on web. The weather demo pushes the current temperature to it every time a forecast loads. The Live Activity is src/widgets/app-activity.tsx, driven by widgets.activity.start / update / end; the weather demo's Show on Lock Screen button starts it and every forecast refresh updates it.
Setup
bun run setup --widgets expo-widgets- Make a dev build:
npx expo run:ios(oreas build --profile development). The widget is an app extension, so Expo Go can't show it. - On the simulator or phone, long-press the Home Screen → Edit → Add Widget → your app → At a glance, small or medium.
- Open the app once so it pushes a first snapshot (the weather demo does this on its home screen).
- Rename it for your app: the
displayNameanddescriptionin theexpo-widgetsplugin entry are what the widget gallery shows. - Run
bun run doctor- every row for this module should be green.
The config plugin adds a second iOS target, <your bundle id>.ExpoWidgetsTarget, and an App Group group.<your bundle id> that the app and the widget share. It also registers both with EAS, so the first eas build asks to create the extra provisioning profile.
Usage
Push a snapshot whenever the value changes:
import { widgets } from "@/lib/widgets";
widgets.update({
title: "Almaty",
value: "21°",
subtitle: "Partly cloudy · H 24° L 12°",
symbol: "cloud.sun.fill", // SF Symbol
});Change the look in src/widgets/app-widget.tsx. Inside the 'widget' function only @expo/ui/swift-ui components, their modifiers and the props work: no hooks, no fetch, no imports from the app, no constants from outside the function. It reads environment.widgetFamily to size itself for systemSmall and systemMedium.
Live Activities
Put the same glance on the Lock Screen and in the Dynamic Island while something is happening - a delivery, a workout, a timer, today's weather:
import { widgets } from "@/lib/widgets";
if (widgets.activity.supported) {
const shown = await widgets.activity.start({
title: "Almaty",
value: "21°",
subtitle: "Rain in 20 min",
symbol: "cloud.rain.fill",
progress: 0.4, // optional bar, 0-1
});
// shown === false: iOS < 16.2, or the user turned Live Activities off for the app
}
await widgets.activity.update({ title: "Almaty", value: "20°" }); // no-op when none is running
await widgets.activity.end();start updates the running activity instead of stacking a second one, and isRunning() finds one left over from a previous launch. The layout - banner, compact leading/trailing, minimal and expanded Dynamic Island regions - is src/widgets/app-activity.tsx, under the same 'widget' rules as the widget. The plugin sets NSSupportsLiveActivities for you. Updates while the app is closed need push-to-update (enablePushNotifications in the plugin options and a server that sends ActivityKit pushes); that isn't wired here.
To add a second widget, write another createWidget("OtherWidget", …) file and add a matching { "name": "OtherWidget", … } entry to the plugin's widgets array.
Gotchas
- The widget never fetches. It shows the last snapshot until the app sends a new one, so update it after every refresh (and from a background task if you add one).
namein the plugin config must equal the first argument ofcreateWidget, and must be a Swift identifier - no spaces or dashes.- Snapshot props replace the previous ones entirely: send every field each time.
- Images need to live in the shared container (
widgetsDirectoryfromexpo-widgets); a bundledrequire()path means nothing to the extension. - A Live Activity lasts at most 8 hours active (12 on the Lock Screen) - iOS ends it after that. Start one for something with an end, not as a permanent widget.
Remove it
While modules/ exists (a tree set up with --keep-modules), setup does all of it:
bun run setup --widgets none --yes --keep-modulesIn a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:
- Delete the files that are still there:
src/lib/__tests__/widgets.test.ts,src/widgets/app-activity.tsx,src/widgets/app-widget.tsx. - Replace, don't delete
src/lib/widgets.ts: core code imports it, so swap in the no-op version frommodules/widgets/none/files/of a fresh clone of your tier repo - same exports, nothing behind them. - Uninstall the dependencies:
bun remove expo-widgets. - Drop the config plugin
expo-widgetsfrom.readynative.json→modules.app.expo.plugins(that is whereapp.config.tsreads it from), then rebuild the dev build. - Check it:
bun run typecheckandbun run lintpoint at anything that still imports the removed files;bun run gen:graphrefreshesdocs/ARCHITECTURE.md.
Reference
Everything below is generated from modules/widgets/expo-widgets/module.json - the same file bun run setup reads, so it is what actually lands in your repo.
Install
bun run setup --widgets expo-widgetsModule id: widgets/expo-widgets (the default for this category).
Dependencies
| Package | Version | Kind |
|---|---|---|
expo-widgets | ~57.0.21 | dependency (expo install) |
Config plugins
Merged into app.config.ts through .readynative.json (modules.app):
expo-widgets(with options)
Doctor checks
- dev build toolchain (Xcode / Android SDK)
After setup
- Widgets are an iOS app extension: make a dev build with
npx expo run:ios(oreas build --profile development). Expo Go cannot show them, andwidgets.update()is a no-op there. - Rename the widget for your app:
displayName/descriptionin theexpo-widgetsplugin entry (modules/widgets/expo-widgets/module.json, or override it in app.config.ts). Keepname: "AppWidget"in step withcreateWidget("AppWidget", …)in src/widgets/app-widget.tsx. - The widget is its own iOS target (
<your bundle id>.ExpoWidgetsTarget) sharing an App Group (group.<your bundle id>) with the app. The plugin registers both with EAS, so the firsteas buildasks to create the extra provisioning profile - say yes.
Files
4 files copied to the project root
src/lib/__tests__/widgets.test.tssrc/lib/widgets.tssrc/widgets/app-activity.tsxsrc/widgets/app-widget.tsx