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.

ACTIVITY
8,432
steps today
68% of goal
systemSmall · 158×158pt
Project StatusUpdated now
12
Open
5
In Review
34
Done
systemMedium · 338×158pt
TodayTuesday, 1 Apr
M
28
T
29
W
30
T
1
F
2
S
3
S
4
Design review
09:00
Sprint planning
14:30
1:1 with Jamie
16:00
Next: Stand-upin 47m
systemLarge · 338×338pt
SizeAPI constantPointsBest forAvoid
SmallsystemSmall158 × 158ptSingle metric, countdown, status indicator, progress ringTables, lists, dense information — space is too tight
MediumsystemMedium338 × 158pt2–3 metrics side-by-side, a short list (3–4 rows), quick action gridLong text, scrolling content (not supported), more than 4 actions
LargesystemLarge338 × 338ptCalendar view, detailed list (5–6 rows), multi-section layoutScrollable lists, deeply nested information, complex forms
Extra LargesystemExtraLarge715 × 338ptiPad only: two-column layouts, rich data dashboardsPhone 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.

FOCUS
72%
22m remaining
Circular progress
Quick Actions
New Task
Log Time
Check In
Report
Quick actions
MetricSmall

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

Status gridMedium

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

Progress ringSmall

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

Quick actionsMedium

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

Chronological listLarge

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

Summary + countdownSmall / Medium

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.

ZoneContent guidance
HeaderApp icon (28×28pt, cornerRadius 8) + short app name or widget title. Keep to one line, 15px semibold max.
Primary valueThe number or status the user came to see. Use the largest legible font size (24–36px for a Small, larger for Medium/Large).
Supporting contextA single line of secondary text — unit, trend indicator, or timestamp. 11–13px, text-tertiary color.
Safe insetMaintain 16pt padding from all edges. WidgetKit enforces this automatically via containerBackground — avoid custom padding overrides.
BackgroundUse .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.

TriggerHow to handle
User opens the appCall WidgetCenter.shared.reloadTimelines(ofKind:). Always refresh after any data mutation.
Background fetchReturn a TimelineEntry array from getTimeline(). The system decides when to execute; request a nextReloadDate no more often than every 15 minutes.
Time-sensitive dataUse TimelinEntry.relevance to signal urgency. High-relevance entries are shown on the Smart Stack's top position.
Network failureAlways return at least one placeholder entry with the last-known data. Never show an empty or broken state — it erodes trust.
Background app refresh offThe widget will not update. Show a subtle "Tap to refresh" deep link so the user can still get current data by tapping through.
TimelineProvider.swift
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.

ActivityWidget.swift
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 · ~44pt

Progress ring, icon + number, single glyph

Sits beside the clock face

accessoryRectangularRect · ~160×48pt

Title + 2–3 lines of secondary text, or title + progress

Below the clock — most legible

accessoryInlineInline · single line

Icon + short string only (e.g. '↑ 8,432 steps')

Top of lock screen, very limited space

Design note

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

Glanceable in 2 seconds

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.

One job per widget

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.

Design for stale data

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.