---
name: glance-design
description: Design native iOS Home Screen widgets in Glance layout language. Apple-like, glanceable. Do not invent chart, webview, or extra node types.
---

# Glance Widget Design Skill

You design **iOS home screen widgets** that feel native, calm, and glanceable — like Apple built them. This skill covers **visual design and composition only**. Workflow, tools, integrations, and sizing math live in the main agent skill.

**Diversity over templates:** Every widget should fit its data and the user's intent. These are principles and patterns — not a single layout to copy. Vary structure, density, and emphasis. Never force one recipe when another fits better.

---

## Design north star

Widgets should feel like **Apple system UI**: clear hierarchy, generous whitespace, restrained color, SF typography. A user glancing at their home screen should read the widget in under two seconds.

| Trait | What it means in practice |
|---|---|
| **Glanceable** | One clear takeaway — a number, status, or headline |
| **Native** | System fonts, semantic colors, no "web app" density |
| **Calm** | Mostly neutral surfaces; color carries meaning, not decoration |
| **Intentional** | Every element earns its space — if it doesn't help, remove it |

---

## Color — Apple-native default, user-guided override

### Default (no user color preference)

Think **Settings / Stocks / Weather** — not marketing landing pages.

| Role | Guidance |
|---|---|
| **Background** | Omit `background` on root for system adaptive fill, or `#FFFFFF` / `#F2F2F7` (light) — never loud gradients unless the user asks |
| **Primary text** | `#000000` or omit `color` (system primary) |
| **Secondary / labels** | `#8E8E93` (iOS secondary label) — captions, timestamps, units |
| **Tertiary / hints** | `#AEAEB2` — de-emphasized metadata only |
| **Accent** | **One** semantic accent per widget — e.g. `#007AFF` (system blue) for links/active, `#34C759` success, `#FF9500` warning, `#FF3B30` alert |
| **Progress / fills** | Muted track `#E5E5EA`, fill = single accent |

**Rules:**
- Never use more than **one accent hue** unless the user requests a colorful or branded look.
- Never fill large areas with saturated color — accents belong on small elements (badge, progress fill, status dot, one headline word).
- Prefer **opacity and weight** over more colors to create hierarchy.

### iOS color format (critical on device)

The iOS renderer only parses **6-digit hex** (`#RRGGBB`). Values like `#FFFFFFCC` or `#FFFFFF40` are **ignored** and fall back to system `.primary` (often black on light widgets).

- Use solid 6-digit hex for every `color`, `background`, `backgroundColor`, `trackColor`, `fillColor`.
- For muted/secondary text on dark or gradient backgrounds, pick a lighter solid tint (e.g. `#FFE8F5` on purple gradients) — not alpha suffixes.
- On colorful widgets, desaturated label colors still beat 8-char hex.

### When the user gives color direction

**Follow the user.** If they say "dark purple theme", "brand pink #E91E8C", or "match my logo colors" — apply their palette to backgrounds, accents, and text while keeping hierarchy and readability. Still use muted variants for labels (`#8E8E93` or a desaturated version of their accent).

If they share no preference, use the Apple-native defaults above.

---

## Typography

Use **SF Pro** (`sf-pro` or omit `fontFamily`). Reserve `sf-rounded` for playful/countdown widgets; `sf-mono` for code/GitHub hashes only.

| Role | fontSize | fontWeight | color |
|---|---|---|---|
| **Hero value** | 24–32 | `bold` or `heavy` | primary |
| **Section title** | 15–17 | `semibold` | primary |
| **Body / detail** | 13–15 | `regular` or `medium` | primary or secondary |
| **Caption / label** | 10–12 | `regular` or `medium` | `#8E8E93` |
| **Unit / suffix** | 11–13 | `medium` | secondary — on `stat` via `suffix` |

**Limits:** At most **3 distinct font sizes** per widget. At most **2 weights** in active use (e.g. semibold + regular).

Always set `maxLines: 1` on titles and hero lines in `small` and `medium` widgets. Use `maxLines: 2` sparingly on `large` only when content demands it.

---

## Spacing rhythm

Pick one rhythm and stick to it for the whole tree:

| Token | Use |
|---|---|
| **8 pt** | Tight gaps inside a row (icon + label) |
| **12 pt** | Root `padding`, default `spacing` within sections |
| **16 pt** | Between major sections on `large` widgets |

Root container: **`padding: 12`** minimum (16 for airy large layouts). Never mix 6 / 10 / 14 randomly.

Use `spacer` with `flex: 1` to push footer elements (badge, timestamp) to the bottom on `large` — don't stack everything at the top.

---

## Visual hierarchy — the one-hero rule

Every widget needs **one focal point**:

| Size | Typical hero |
|---|---|
| `small` | One `stat` value, one large number `text`, or one `avatar` + short label |
| `medium` | Hero left or top + supporting detail opposite/below |
| `large` | Hero row + 2–4 supporting metrics or a content block |

Everything else is **supporting** — smaller, lighter color, fewer lines.

**Do not** give equal visual weight to five text lines. If everything is bold and 16 pt, nothing is important.

---

## Static vs dynamic bindings

Only `{{variable_name}}` becomes a **content field** (upload form, automation ingest, `bindings_required`).

| Use case | Template syntax | In derived schema? |
|---|---|---|
| Live data (username, KPI value, event title) | `"binding": "{{username}}"` | Yes |
| Fixed section label ("Profile views today") | `"binding": "Profile views today"` | No |
| Fixed metric name under a ring ("Revenue") | `"binding": "Revenue"` | No |
| Decorative brand icon (design constant) | `"binding": "https://…/icon.png"` on `image` | No |
| SF Symbol | `"binding": "bag.fill"` on `icon` | No |
| Progress-dependent caption | `"binding": "{{overall_goal_caption}}"` | Yes |

**Rule:** If the copy or asset never changes per refresh, use a **plain literal** — not `{{}}`. This keeps the upload flow clean and matches how the goldens are authored.

**Static images on device:** use **HTTPS PNG/JPEG** URLs. Local paths like `/brand-icons/…` only work in React preview.

---

## Golden reference library

Six device-approved examples live in `packages/sdk/goldens/`. They are the **quality bar** — adapt their structure to the user's data; never paste one verbatim with wrong bindings.

**Before designing:** call `list_golden_examples` (filter by `widget_size` or fetch one with `example_id`).

| ID | Size | Pattern | When to adapt |
|---|---|---|---|
| **s1-glance-kpi** | small | Gradient + username + 2 stacked KPI stats + static brand icon | Social/account KPIs, branded small metrics |
| **s2-glance-media** | small | White card: static section label → title → centered thumbnail → caption | Latest post/video/article thumbnails |
| **m1-split-strip** | medium | Left: static title stack · Right: hero `stat` | Single headline metric strips (monitoring, uptime) |
| **m2-multi-source** | medium | Left motion panel + right spend card + calendar event rows | Multi-integration dashboards (device + cloud) |
| **l1-app-performance** | large | Dark 2×2 KPI rings (`z-stack` + `circular_progress`) + quarterly `progress_bar` | Custom business metrics with goal progress |
| **l2-outfit-of-the-day** | large | Gradient + hero image + shoppable item rows (`href` + `icon`) | Creator commerce, product lists, playful layouts |

### Golden techniques worth copying

- **S1:** `gradient` root; static caption literals above dynamic `stat` values; trailing brand `image` with static URL
- **S2:** explicit `spacer` gaps between title/image/caption; `maxLines` on all text; white `#FFFFFF` root background
- **M1:** horizontal split with `flex: 1` text column + trailing hero `stat` (`valueFontSize: 40`)
- **M2:** nested gray cards (`#F2F2F7`); color accent bars beside calendar rows; formatted strings in `text` not `stat`
- **L1:** ring container **82×82** with `circular_progress` **size 70, lineWidth 5** (prevents stroke clip); value text centered in `z-stack`; `overall_goal_caption` stays dynamic
- **L2:** `gradient` with `spacing` as gap; `fillContainer` hero image; `href` on shop tap targets; `sf-rounded` for playful headers

---

## Size patterns (adapt — do not copy blindly)

Prefer picking a **golden reference** above, then adapt. These ASCII sketches are fallbacks when no golden fits:

### Small (170×170) — "one glance"

Best for: single KPI, countdown, status, one image thumbnail.

```
vertical container, padding 12, spacing 8
  → caption text (12pt, secondary, maxLines 1)     // label
  → stat OR large text (hero, 26–30pt)             // THE number/word
  → optional: badge OR relative_time (11pt)        // context
```

Avoid: horizontal rows of 3+ items, multiple stats, long paragraphs.

### Medium (364×170) — "headline strip"

Best for: integration dashboard, social snapshot, calendar next event.

**Pattern A — split row:**
```
vertical, padding 12
  → horizontal row, spacing 12, alignment center
      → left column (vertical): title + subtitle
      → right: stat OR circular_progress OR avatar/image
```

**Pattern B — title + detail row:**
```
vertical, padding 12, spacing 8
  → title text (15pt semibold, maxLines 1)
  → horizontal row: 2–3 compact stats OR image + text pair
```

Avoid: more than one horizontal band; cramming 4 columns.

### Large (364×382) — "dashboard card"

Best for: multi-metric boards, feed digests, rich integration views.

```
vertical, padding 12–16, spacing 12
  → header row (horizontal): title + optional badge
  → main content: 2–3 stats in a row OR 2×2 grid of images/metrics
  → spacer (flex 1)                                    // optional
  → footer: progress_bar OR relative_time OR caption   // optional
```

Use `divider` between header and body only when it clarifies — not by default.

---

## Component combinations that look polished

| Combo | When to use |
|---|---|
| `stat` + caption `text` above | KPI widgets (followers, revenue, streak) |
| `avatar` + vertical `text` stack in horizontal row | Social / account identity |
| `image` (cornerRadius 8) + `text` beside | Latest post, thumbnail + caption |
| `badge` (subtle bg `#E5E5EA`, text primary) | Status chips — "Live", "3 new" |
| `progress_bar` (height 6–8, muted track) | Goal completion — one per widget |
| `z-stack` + full-bleed `image` + `text` with light color | Event spotlight — use sparingly |
| `status_dot` + `text` | Health / server / pipeline status |
| `relative_time` or `date` in caption style | "Updated 2m ago" footer |

**`stat` tips:** Use `valueFontSize` 26–32 for hero stats; `labelFontSize` 11; `suffix` for units (`K`, `%`, `°`). Prefer `ig_followers_fmt` style formatted strings in `text` when the integration provides them.

---

## Layout polish

- **Alignment:** `leading` for LTR text blocks; `center` for single hero in small; `trailing` for meta (time, badge) in header rows — but see reload button rule below.
- **Reload button:** Glance overlays a reload control in the **top-right** (~40×40 pt). Never put hero values or must-read text there; prefer leading clusters for top-band KPIs (label + value side-by-side).
- **Images:** Always set explicit `width` / `height`; `cornerRadius: 8–12` for photos; `contentMode: "fill"` for thumbnails.
- **Nested cards:** Inner `container` with `background: "#F2F2F7"`, `cornerRadius: 10`, `padding: 10` — sparingly, one level deep max.
- **Full bleed:** Only `gradient` root for dramatic widgets — and only when user wants flair or data suits it (event poster, media highlight).

---

## Anti-patterns — never ship these

| Anti-pattern | Why it fails |
|---|---|
| 5+ `text` nodes all 14 pt regular | No hierarchy — wallpaper of words |
| Rainbow hex on every element | Feels non-native, noisy |
| Horizontal row of 4 images on `small` | Guaranteed clip / crush |
| `stat` for currency (`$1,234`) or `%` strings | Type errors + ugly fallback |
| Root `padding: 0` or missing | Corner clip on home screen |
| Hero stat / spend in top-trailing corner | Hidden under reload button overlay |
| `gap`, `borderRadius`, `textAlign` style keys | Silently ignored — use correct keys from main skill |
| `{{}}` on fixed labels or decorative icons | Forces upload fields; use static literals instead |
| 8-char hex colors (`#RRGGBBAA`) | iOS ignores them — text falls back to black primary |

---

## Design process (before you write JSON)

1. **Clarify intent** — glance KPI? digest? status? media?
2. **Pick `widget_size`** — smallest size that fits without clutter.
3. **Choose one hero** — what is the single takeaway?
4. **`list_golden_examples`** — pick the closest golden; call with `example_id` to load its tree.
5. **Adapt the pattern** — swap bindings for real integration fields; static labels stay literal.
6. **Assign typography** — hero / body / caption sizes from table.
7. **Apply color** — native defaults unless user specified palette; **6-digit hex only**.
8. **Run sizing math** — main skill algorithm; shrink before dropping the hero.
9. **(Optional) `validate_template`** — max **2 calls** per design for early feedback; then `create_template`. Structure must pass; design warnings do not block save.
10. **Preview pause** — describe what the user will see; invite tweaks.

---

## Pre-submit checklist

Before `create_template`, confirm mentally (server **structure** is the save gate; padding/overflow are warnings):

- [ ] One clear hero element
- [ ] ≤ 3 font sizes, ≤ 2 active weights
- [ ] Root `padding` ≥ 12 (guideline — `ROOT_PADDING_LOW` will not block save)
- [ ] Labels use secondary color (`#8E8E93` or user-muted variant)
- [ ] At most one accent color (unless user asked for more)
- [ ] `maxLines` set on text that must not wrap
- [ ] Top-trailing corner clear of hero values (reload button overlay)
- [ ] Static labels/icons use plain literals (no `{{}}` unless value changes per refresh)
- [ ] All colors are 6-digit `#RRGGBB` (no alpha suffix)
- [ ] Layout suits the **data** — not a generic placeholder grid

---

## Talking to the user about design

- Describe the **visual hierarchy** in plain language: *"Large follower count up top, latest post caption below in gray."*
- Offer **one alternative** when useful: *"I can also do a side-by-side layout with your profile photo — prefer that?"*
- If the user says it looks plain, add **structure** (nested card, badge, progress) or **typography contrast** — not more random colors.
