Live Activities
A Live Activity surfaces one in-progress event — a delivery, a timer, a live score — on the Lock Screen and in the Dynamic Island, updating in place without the user opening the app. Built on ActivityKit, not WidgetKit, and governed by a different set of visual constraints than home screen widgets.
Not the same surface as widgets
Mobile Widgets are static, timeline-refreshed snapshots built on WidgetKit, and can use .containerBackground(for: .widget)— the system material that handles light/dark and vibrancy automatically. Live Activities are built on ActivityKit for a single, currently-running event, are pushed or locally updated in near-real time, and end when the event does. Because the Dynamic Island and lock screen present outside the normal app chrome, they structurally cannot use System Materials at all — the constraint below isn't a stylistic choice, it's the only option ActivityKit gives you.
Don't try to port Sitka's card/glass tokens onto a Live Activity. Treat it as its own small design system with exactly two ingredients — one solid tint color and white text at two opacities — and put your effort into choosing a tint that reads clearly on both the Dynamic Island's black surface and the Lock Screen's wallpaper.
Regions
A single Live Activity configuration renders in up to five places. Design them together — the same underlying state (a title, a detail line, a live value) is what populates all five, just at different sizes.
| Region | Where it appears | What to put there |
|---|---|---|
| Minimal | Shown when a second Live Activity is also active and competing for Dynamic Island space. | A single glyph only — no text fits. |
| Compact leading | Left side of the pill, always visible while the activity is running. | One small icon or glyph identifying the activity. |
| Compact trailing | Right side of the pill. | The single most important live value — usually a countdown or short status. |
| Expanded leading | Top-left when the user long-presses the island. | Icon + short title. |
| Expanded trailing | Top-right when expanded. | The same live value as compact trailing, at a larger size. |
| Expanded bottom | Full-width row beneath the top regions when expanded. | One line of supporting detail — a destination, a subtitle, a next action. |
| Lock screen banner | Full-width card on the lock screen and in Notification Center. | The complete activity: title, one line of detail, and the live value. This is the only region with real horizontal space — expanded and lock screen typically share the same view. |
Design constraints
Dynamic Island and lock screen surfaces render outside your app's normal view hierarchy and can't use System Materials, vibrancy, or Sitka's card/glass chrome. Set a single solid background color with .activityBackgroundTint(_:) — that color is the entire visual language available to you.
With no material to lean on, legibility over an arbitrary background comes from contrast, not chrome. Pick a tint dark enough that white text at ~90–100% opacity for primary content and ~55–70% for secondary clears WCAG AA — don't try to reuse light-mode/dark-mode text tokens here.
Compact and minimal have almost no space — a glyph and a handful of characters. Design those first, then expand outward to the lock screen banner, not the other way around; content that only fits the banner has nowhere to go in the compact states.
SwiftUI implementation
ActivityKit is iOS/watchOS-only — there is no React or HTML equivalent, unlike most patterns in this library. A minimal Live Activity needs an ActivityAttributes type (the fixed data, set once) plus a ContentState (the part that updates), and a Widget conformance that lays out the lock screen view and every Dynamic Island region.
import ActivityKit
import WidgetKit
import SwiftUI
// 1. Attributes — fixed for the activity's lifetime
struct DeliveryAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
var eta: Date
var address: String
}
var orderId: String
}
// 2. Widget — lays out lock screen + every Dynamic Island region
struct DeliveryLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
// Lock screen / banner
HStack {
VStack(alignment: .leading, spacing: 2) {
Text("Out for delivery").font(.headline).foregroundStyle(.white)
Text(context.state.address)
.font(.caption)
.foregroundStyle(.white.opacity(0.65))
}
Spacer()
Text(timerInterval: Date.now...context.state.eta, countsDown: true)
.font(.title2.monospacedDigit())
.foregroundStyle(.white)
}
.padding()
// The ONLY chrome available — a flat tint, no material.
.activityBackgroundTint(Color(red: 0.30, green: 0.12, blue: 0.03))
.activitySystemActionForegroundColor(.white)
} dynamicIsland: { context in
DynamicIsland {
DynamicIslandExpandedRegion(.leading) {
Label("Out for delivery", systemImage: "shippingbox.fill")
.lineLimit(1)
}
DynamicIslandExpandedRegion(.trailing) {
Text(timerInterval: Date.now...context.state.eta, countsDown: true)
.font(.caption.monospacedDigit())
}
DynamicIslandExpandedRegion(.bottom) {
Text(context.state.address).font(.caption2).foregroundStyle(.secondary)
}
} compactLeading: {
Image(systemName: "shippingbox.fill")
} compactTrailing: {
Text(timerInterval: Date.now...context.state.eta, countsDown: true)
.font(.caption2.monospacedDigit())
.frame(width: 44)
} minimal: {
Image(systemName: "shippingbox.fill")
}
}
}
}Real-world reference: JobFlo ships two Live Activities on this same shape — an interview countdown and an application-deadline countdown — each with its own tint color and SF Symbol, structured identically to the example above.
Accessibility
- →VoiceOver reads the Dynamic Island and lock screen banner as a unit — provide an .accessibilityLabel summarizing the full state ('Out for delivery, arriving in 12 minutes') rather than letting it read each Text view separately.
- →The countdown timer text (Text(timerInterval:countsDown:)) updates its accessible value automatically — don't wrap it in a custom label that goes stale relative to the visible time.
- →Never rely on the tint color alone to convey status (e.g. red = urgent) — pair any status change with a text or icon change, since the tint is a single fixed value for the activity's lifetime in most implementations.
- →Keep the compact and minimal region content meaningful on its own — some users only ever see the collapsed Dynamic Island, never the expanded view.
- →Always set a sensible staleness date and end the activity promptly when the event concludes — a Live Activity that outlives its event (a delivery marked 'arriving' long after it arrived) erodes trust in every future one your app shows.