Skip to main content
Widgets are the most popular target type, enabling home screen experiences, Live Activities, Dynamic Island, and watch complications.

Widget types

The widget target type supports multiple WidgetKit features:

Home Screen Widgets

Static and dynamic widgets on iOS and iPadOS home screens

Live Activities

Real-time updates in Dynamic Island and Lock Screen

Control Widgets

Interactive controls in Control Center and Lock Screen

Smart Stacks

Context-aware widget rotation in widget stacks

Extension point

PropertyValue
Extension Pointcom.apple.widgetkit-extension
Product Typecom.apple.product-type.app-extension
FrameworksWidgetKit, SwiftUI, ActivityKit, AppIntents
App GroupsEnabled by default

Creating a widget

1

Run create-target

Generate a new widget target:
npx create-target widget
2

Configure the widget

Edit targets/widget/expo-target.config.js:
/** @type {import('@bacons/apple-targets/app.plugin').Config} */
module.exports = {
  type: "widget",
  icon: "./assets/widget-icon.png",
  colors: {
    $widgetBackground: "#DB739C",
    $accent: "#F09458",
  },
  entitlements: {
    "com.apple.security.application-groups": ["group.com.myapp.data"],
  },
};
3

Implement the widget

Edit targets/widget/Widget.swift with your SwiftUI implementation.
4

Rebuild the project

npx expo prebuild -p ios --clean
5

Build and test

Open in Xcode and build the widget target:
xed ios

Widget colors

Widgets use two special colors with corresponding Xcode build settings:
Color NameBuild SettingPurpose
$widgetBackgroundASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAMEWidget background color
$accentASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAMETint color for buttons and interactive elements
Define colors in your config:
colors: {
  // Simple color
  $widgetBackground: "#DB739C",
  
  // Light/dark mode
  $accent: {
    light: "#F09458",
    dark: "#3E72A0",
  },
  
  // Custom colors for SwiftUI
  gradient1: "#E4975D",
  gradient2: "#3E72A0",
}

Live Activities

Live Activities require ActivityKit framework (included by default):
import ActivityKit
import WidgetKit
import SwiftUI

struct MyActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: MyActivityAttributes.self) { context in
            // Lock screen / banner UI
            VStack {
                Text("Status: \(context.state.status)")
            }
        } dynamicIsland: { context in
            // Dynamic Island UI
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    Text("Leading")
                }
            }
        }
    }
}
Start a Live Activity from your React Native app:
// You'll need to use a native module to start Live Activities
// The widget extension displays the activity

Control Widgets

Control widgets appear in Control Center (iOS 18+):
import AppIntents
import WidgetKit

@available(iOS 18.0, *)
struct MyControlWidget: ControlWidget {
    static let kind: String = "com.myapp.control"
    
    var body: some ControlWidgetConfiguration {
        StaticControlConfiguration(kind: Self.kind) {
            ControlWidgetButton(action: OpenAppIntent()) {
                Label("Open App", systemImage: "star")
            }
        }
        .displayName("My Control")
        .description("Quick action for my app")
    }
}

@available(iOS 18.0, *)
struct OpenAppIntent: ControlConfigurationIntent {
    static let title: LocalizedStringResource = "Open App"
    static let openAppWhenRun: Bool = true
    
    func perform() async throws -> some IntentResult & OpensIntent {
        return .result(opensIntent: OpenURLIntent(
            URL(string: "myapp://settings")!
        ))
    }
}
Control widget intents must be in the _shared folder so they’re linked to both the widget extension and main app target. See Shared Files.
Reload controls from React Native:
import { ExtensionStorage } from "@bacons/apple-targets";

ExtensionStorage.reloadControls();

Sharing data with widgets

Use App Groups to share data between your app and widget:
import { ExtensionStorage } from "@bacons/apple-targets";

const storage = new ExtensionStorage("group.com.myapp.data");

// Set data
storage.set("message", "Hello from the app!");
storage.set("counter", 42);

// Reload the widget to display new data
ExtensionStorage.reloadWidget();
Access the data in your widget:
let defaults = UserDefaults(suiteName: "group.com.myapp.data")
let message = defaults?.string(forKey: "message") ?? "No data"
let counter = defaults?.integer(forKey: "counter") ?? 0

Watch widgets

For Apple Watch complications, use the watch-widget target type instead:
npx create-target watch-widget
Watch widgets use the same WidgetKit framework but with watch-specific layouts.

Best practices

  • Keep widget code lightweight
  • Use TimelineProvider efficiently
  • Minimize network requests
  • Cache images and data
  • Show the most important information first
  • Use clear typography and spacing
  • Support all widget sizes (small, medium, large)
  • Test in both light and dark mode
  • Use App Groups for data sharing
  • Call reloadWidget() after data changes
  • Implement background updates for time-sensitive data
  • Handle missing or stale data gracefully

Troubleshooting

On iOS 18+, long-press your app icon and select widget display options to transform the app icon into the widget. On earlier iOS versions, add the widget from the home screen widget picker.
Clear the SwiftUI preview cache:
xcrun simctl --set previews delete all
Verify:
  • App Groups entitlement is correctly configured
  • You’re calling ExtensionStorage.reloadWidget()
  • The widget’s TimelineProvider is returning new entries

Production example

Check out Evan Bacon’s Pillar Valley widget for a real-world implementation.

Learn more

Sharing data

Share data between app and widget

Control widgets

Build Control Center widgets

Widget guide

Detailed widget implementation guide

Apple documentation

Official WidgetKit documentation

Build docs developers (and LLMs) love