ReadyNative
Home-screen widgets

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
  1. Make a dev build: npx expo run:ios (or eas build --profile development). The widget is an app extension, so Expo Go can't show it.
  2. On the simulator or phone, long-press the Home Screen → Edit → Add Widget → your app → At a glance, small or medium.
  3. Open the app once so it pushes a first snapshot (the weather demo does this on its home screen).
  4. Rename it for your app: the displayName and description in the expo-widgets plugin entry are what the widget gallery shows.
  5. 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).
  • name in the plugin config must equal the first argument of createWidget, 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 (widgetsDirectory from expo-widgets); a bundled require() 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-modules

In a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:

  1. Delete the files that are still there: src/lib/__tests__/widgets.test.ts, src/widgets/app-activity.tsx, src/widgets/app-widget.tsx.
  2. Replace, don't delete src/lib/widgets.ts: core code imports it, so swap in the no-op version from modules/widgets/none/files/ of a fresh clone of your tier repo - same exports, nothing behind them.
  3. Uninstall the dependencies: bun remove expo-widgets.
  4. Drop the config plugin expo-widgets from .readynative.json → modules.app.expo.plugins (that is where app.config.ts reads it from), then rebuild the dev build.
  5. Check it: bun run typecheck and bun run lint point at anything that still imports the removed files; bun run gen:graph refreshes docs/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-widgets

Module id: widgets/expo-widgets (the default for this category).

Dependencies

PackageVersionKind
expo-widgets~57.0.21dependency (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

  1. Widgets are an iOS app extension: make a dev build with npx expo run:ios (or eas build --profile development). Expo Go cannot show them, and widgets.update() is a no-op there.
  2. Rename the widget for your app: displayName / description in the expo-widgets plugin entry (modules/widgets/expo-widgets/module.json, or override it in app.config.ts). Keep name: "AppWidget" in step with createWidget("AppWidget", …) in src/widgets/app-widget.tsx.
  3. 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 first eas build asks to create the extra provisioning profile - say yes.

Files

4 files copied to the project root
  • src/lib/__tests__/widgets.test.ts
  • src/lib/widgets.ts
  • src/widgets/app-activity.tsx
  • src/widgets/app-widget.tsx

On this page

Get ReadyNative