Widgets bring your app’s content directly to the home screen, lock screen, and Dynamic Island. With Expo Apple Targets, you can build WidgetKit widgets using Swift and SwiftUI while keeping your source code outside the generated iOS directory.
Evan Bacon wrote an in-depth blog post about building widgets in production: Expo x Apple Widgets . The Pillar Valley game uses this plugin for production widgets.
Getting Started
Generate a widget target
Run the create-target CLI to scaffold a new widget: This creates a targets/widget/ directory with:
expo-target.config.js — Target configuration
index.swift — Main widget bundle
widgets.swift — Widget implementation
AppIntent.swift — Configurable parameters
WidgetLiveActivity.swift — Live Activities support
WidgetControl.swift — Control Center widgets
Configure the target
Edit targets/widget/expo-target.config.js: /** @type {import('@bacons/apple-targets/app.plugin').Config} */
module . exports = {
type: "widget" ,
icon: "../../icons/widget.png" ,
colors: {
// Widget background color (referenced in Info.plist)
$widgetBackground: "#DB739C" ,
// Global accent color for buttons and interactive elements
$accent: "#F09458" ,
// Custom colors for SwiftUI
gradient1: {
light: "#E4975D" ,
dark: "#3E72A0" ,
},
},
images: {
valleys: "../../valleys.png" ,
},
entitlements: {
"com.apple.security.application-groups" : [ "group.bacon.data" ],
},
};
Run prebuild
Generate the Xcode project with the widget linked: npx expo prebuild -p ios --clean
Develop in Xcode
Open the project and find your widget under expo:targets/widget: All changes you make in this folder are saved to targets/widget/ outside the iOS directory.
Here’s the default widget structure generated by the template:
targets/widget/widgets.swift
targets/widget/AppIntent.swift
targets/widget/index.swift
import WidgetKit
import SwiftUI
struct Provider : AppIntentTimelineProvider {
func placeholder ( in context : Context) -> SimpleEntry {
SimpleEntry ( date : Date (), configuration : ConfigurationAppIntent ())
}
func snapshot ( for configuration : ConfigurationAppIntent, in context : Context) async -> SimpleEntry {
SimpleEntry ( date : Date (), configuration : configuration)
}
func timeline ( for configuration : ConfigurationAppIntent, in context : Context) async -> Timeline<SimpleEntry> {
var entries: [SimpleEntry] = []
// Generate a timeline consisting of five entries an hour apart
let currentDate = Date ()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar. current . date ( byAdding : . hour , value : hourOffset, to : currentDate) !
let entry = SimpleEntry ( date : entryDate, configuration : configuration)
entries. append (entry)
}
return Timeline ( entries : entries, policy : . atEnd )
}
}
struct SimpleEntry : TimelineEntry {
let date: Date
let configuration: ConfigurationAppIntent
}
struct widgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text ( "Time:" )
Text (entry. date , style : . time )
Text ( "Favorite Emoji:" )
Text (entry. configuration . favoriteEmoji )
}
}
}
struct widget : Widget {
let kind: String = "widget"
var body: some WidgetConfiguration {
AppIntentConfiguration ( kind : kind, intent : ConfigurationAppIntent. self , provider : Provider ()) { entry in
widgetEntryView ( entry : entry)
. containerBackground (. fill . tertiary , for : . widget )
}
}
}
Colors
The plugin provides special color constants that map to Xcode build settings:
Color Name Build Setting Purpose $accentASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAMEGlobal accent color for buttons when editing the widget $widgetBackgroundASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAMEWidget background color
You can also define custom colors for use in SwiftUI:
colors : {
$widgetBackground : "#DB739C" ,
$accent : "#F09458" ,
// Custom colors with light/dark mode support
gradient1 : {
light : "#E4975D" ,
dark : "#3E72A0" ,
},
// Or use a single color
primaryColor : "steelblue" ,
}
Access these colors in Swift:
Color ( "gradient1" )
Color ( "primaryColor" )
Live Activities
Live Activities appear on the lock screen and in the Dynamic Island for real-time updates:
targets/widget/WidgetLiveActivity.swift
import ActivityKit
import WidgetKit
import SwiftUI
struct WidgetAttributes : ActivityAttributes {
public struct ContentState : Codable , Hashable {
// Dynamic stateful properties
var emoji: String
}
// Fixed non-changing properties
var name: String
}
struct WidgetLiveActivity : Widget {
var body: some WidgetConfiguration {
ActivityConfiguration ( for : WidgetAttributes. self ) { context in
// Lock screen/banner UI
VStack {
Text ( "Hello \( context. state . emoji ) " )
}
. activityBackgroundTint (Color. cyan )
. activitySystemActionForegroundColor (Color. black )
} dynamicIsland : { context in
DynamicIsland {
// Expanded UI regions
DynamicIslandExpandedRegion (. leading ) {
Text ( "Leading" )
}
DynamicIslandExpandedRegion (. trailing ) {
Text ( "Trailing" )
}
DynamicIslandExpandedRegion (. bottom ) {
Text ( "Bottom \( context. state . emoji ) " )
}
} compactLeading : {
Text ( "L" )
} compactTrailing : {
Text ( "T \( context. state . emoji ) " )
} minimal : {
Text (context. state . emoji )
}
. widgetURL ( URL ( string : "https://www.expo.dev" ))
. keylineTint (Color. red )
}
}
}
Trigger widget updates from your React Native app:
import { ExtensionStorage } from "@bacons/apple-targets" ;
// Reload all widgets
ExtensionStorage . reloadWidget ();
// Reload a specific widget by kind
ExtensionStorage . reloadWidget ( "widget" );
See the Sharing Data guide for details on syncing data between your app and widget.
Troubleshooting
If you experience build issues, React Native’s uncompiled source can cause problems with the Swift compiler and SwiftUI previews.
Clear SwiftUI preview cache
xcrun simctl --set previews delete all
Prebuild without React Native
For faster iteration during widget development:
npx expo prebuild --template ./node_modules/@bacons/apple-targets/prebuild-blank.tgz --clean
This is for development only , not production builds.
On iOS 18+, long press your app icon and select widget display options to transform the app icon into the widget.
Real-World Example
The Pillar Valley game uses this plugin for production widgets. Check out the blog post for implementation details: Expo x Apple Widgets .