Mobile Widgets
Home screen and lock screen widgets surface a single, glanceable piece of information without opening the app. Four sizes, strict layout constraints, and a timeline-based data model distinguish them from every other UI surface.
Sizes
WidgetKit defines four canonical sizes. Offer all sizes that make sense for your content — users choose based on how much home screen space they want to trade. Every widget must support at least systemSmall.
| Size | API constant | Points | Best for | Avoid |
|---|---|---|---|---|
| Small | systemSmall | 158 × 158pt | Single metric, countdown, status indicator, progress ring | Tables, lists, dense information — space is too tight |
| Medium | systemMedium | 338 × 158pt | 2–3 metrics side-by-side, a short list (3–4 rows), quick action grid | Long text, scrolling content (not supported), more than 4 actions |
| Large | systemLarge | 338 × 338pt | Calendar view, detailed list (5–6 rows), multi-section layout | Scrollable lists, deeply nested information, complex forms |
| Extra Large | systemExtraLarge | 715 × 338pt | iPad only: two-column layouts, rich data dashboards | Phone apps — this size is iPad-exclusive |
Content types
Most widgets fall into one of four patterns. Match the pattern to your data before designing the layout.
One primary number with unit and a simple progress bar or delta indicator. The number should read instantly — don't make the user calculate.
8,432 steps · 68% of goal
2–4 named values arranged in a grid. Useful for dashboards where multiple counts need comparison at a glance.
Open / In Review / Done issue counts
A circular arc represents completion percentage. More expressive than a bar for goal-based data — the circular shape immediately signals a bounded task.
72% Focus session complete
A grid of tappable deep links into specific app actions. Each cell is a Link targeting a specific URL scheme. Limit to 4 actions — more creates choice paralysis.
New Task / Log Time / Check In / Report
3–6 time-ordered items (events, tasks, notifications). Show time, title, and a single-color accent. Truncate long titles to one line.
Today's calendar events
A primary value plus relative time string ("in 47m", "2 days left"). Best when the user's key question is 'when'. Update the timeline entry at each transition.
Next meeting in 47 minutes
Layout zones
Every widget size shares the same four-zone anatomy. Arrange content top-to-bottom in this order so users build a consistent mental model across all your widgets.
| Zone | Content guidance |
|---|---|
| Header | App icon (28×28pt, cornerRadius 8) + short app name or widget title. Keep to one line, 15px semibold max. |
| Primary value | The number or status the user came to see. Use the largest legible font size (24–36px for a Small, larger for Medium/Large). |
| Supporting context | A single line of secondary text — unit, trend indicator, or timestamp. 11–13px, text-tertiary color. |
| Safe inset | Maintain 16pt padding from all edges. WidgetKit enforces this automatically via containerBackground — avoid custom padding overrides. |
| Background | Use .containerBackground(for: .widget) in SwiftUI — it handles vibrancy, dark/light, and StandBy mode automatically. |
Timeline and refresh
Widgets are not real-time views. They render a snapshot from a TimelineEntry the system provided, possibly minutes ago. Design for this model — never assume the widget reflects the current moment.
| Trigger | How to handle |
|---|---|
| User opens the app | Call WidgetCenter.shared.reloadTimelines(ofKind:). Always refresh after any data mutation. |
| Background fetch | Return a TimelineEntry array from getTimeline(). The system decides when to execute; request a nextReloadDate no more often than every 15 minutes. |
| Time-sensitive data | Use TimelinEntry.relevance to signal urgency. High-relevance entries are shown on the Smart Stack's top position. |
| Network failure | Always return at least one placeholder entry with the last-known data. Never show an empty or broken state — it erodes trust. |
| Background app refresh off | The widget will not update. Show a subtle "Tap to refresh" deep link so the user can still get current data by tapping through. |
struct ProjectStatusProvider: TimelineProvider {
func getTimeline(
in context: Context,
completion: @escaping (Timeline<ProjectEntry>) -> Void
) {
Task {
// Fetch fresh data from your service
let status = try? await ProjectService.fetchStatus()
let entry = ProjectEntry(
date: .now,
status: status ?? .placeholder
)
// Reload no more than every 15 minutes
let nextUpdate = Calendar.current.date(
byAdding: .minute, value: 15, to: .now
)!
let timeline = Timeline(
entries: [entry],
policy: .after(nextUpdate)
)
completion(timeline)
}
}
}SwiftUI implementation
A minimal WidgetKit widget requires three types: a TimelineEntry, a TimelineProvider, and a SwiftUI view. The widget struct wires them together.
import WidgetKit
import SwiftUI
// 1. Entry — the data snapshot
struct ActivityEntry: TimelineEntry {
let date: Date
let steps: Int
let goal: Int
var progress: Double { min(Double(steps) / Double(goal), 1) }
var isPlaceholder: Bool { steps == 0 }
}
// 2. View — renders the snapshot
struct ActivityWidgetView: View {
var entry: ActivityEntry
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 6) {
Image(systemName: "figure.walk")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.accent)
Text("ACTIVITY")
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(.secondary)
.tracking(0.04)
}
Spacer()
Text(entry.steps, format: .number)
.font(.system(size: 34, weight: .bold, design: .rounded))
.foregroundStyle(.primary)
Text("steps today")
.font(.system(size: 12))
.foregroundStyle(.secondary)
ProgressView(value: entry.progress)
.tint(.accent)
.padding(.top, 8)
Text("\(Int(entry.progress * 100))% of goal")
.font(.system(size: 10))
.foregroundStyle(.secondary)
.padding(.top, 4)
}
.padding(16)
.containerBackground(for: .widget) { Color(.systemBackground) }
}
}
// 3. Widget definition
struct ActivityWidget: Widget {
let kind = "ActivityWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: ActivityProvider()) { entry in
ActivityWidgetView(entry: entry)
}
.configurationDisplayName("Activity")
.description("Today's step count and goal progress.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}Lock screen widgets
iOS 16+ supports lock screen widgets in three families. They render in monochrome by default and must read clearly with no color. Use SF Symbols for icons — they adapt to vibrancy automatically.
accessoryCircularCircle · ~44ptProgress ring, icon + number, single glyph
Sits beside the clock face
accessoryRectangularRect · ~160×48ptTitle + 2–3 lines of secondary text, or title + progress
Below the clock — most legible
accessoryInlineInline · single lineIcon + short string only (e.g. '↑ 8,432 steps')
Top of lock screen, very limited space
Lock screen widgets render with system vibrancy — the OS composites your content over the wallpaper using luminosity blending. Never use color to convey meaning; use shape, size, and text. Test your widget on multiple wallpapers in both dark and light conditions.
Design principles
A widget competes with the clock and notification badges for attention. If your most important value takes more than two seconds to read, the widget is too dense.
Pick the single piece of information your user checks most often. If you find yourself needing to show three unrelated things, ship three separate widgets and let the user pick.
Widgets are not live — they reflect a snapshot. Always show a timestamp or relative label ("Updated 4m ago") so the user knows how fresh the data is.
Accessibility
- →VoiceOver reads widgets as a whole unit by default. Use .accessibilityLabel on the top-level view to provide a concise summary: "8,432 steps, 68 percent of daily goal." Don't make VoiceOver navigate through individual labels and values.
- →Dynamic Type does not apply to widget text directly — widgets use fixed point sizes. Ensure your minimum text size (typically 11pt) remains readable at the default display size.
- →Avoid conveying information through color alone on lock screen widgets — they render in monochrome. On home screen widgets, pair color with a label or icon.
- →All tappable regions must have a Link wrapping a meaningful URL. The entire widget can be a single tap target, or sub-regions can be independent Links (Medium and larger only).
- →Use .widgetURL for a single tap target on any size. Use Link views for multiple tap targets — only available on systemMedium and larger.
- →Test with Reduce Transparency and Increase Contrast enabled. The .containerBackground modifier handles most cases automatically when using system materials.