Widget UX patterns
An agent widget is a guest in someone else's interface. It inherits their theme,
competes with their composer, and gets closed the moment it feels like a
different app. Design to the container.
Start with the smallest presentation that lets the user understand the result
or finish the task. Request more space only when the workflow demands it.
The four display modes
Inline card
A focused result, a confirmation, or a small set of actions. The default.
- Maximum two primary actions — one primary CTA, one optional secondary.
- No deep navigation, no tabs, no drill-ins inside the card.
- No nested scrolling. Auto-fit content to the available height.
- Never duplicate a feature the host already provides.
- Preserve visual hierarchy: headline → supporting text → CTA.
Mobile: the card may expand its height to match content, up to the height of the
mobile display area.
Inline carousel
Scanning a small set of similar, visually rich options — restaurants, playlists,
events.
- 3–8 items. Fewer looks broken; more is a list, not a carousel.
- ≤3 lines of metadata per item.
- One clear CTA per item.
- Consistent aspect ratios. A ragged carousel reads as a bug.
Fullscreen
Rich tasks needing room: maps, editing canvases, detailed browsing.
- The host's composer stays overlaid. Design around it — never place
controls where the composer lands.
- Do not replicate a native app wholesale. Fullscreen is a focused surface, not
a port of your product.
- Provide an obvious way back to inline.
- In M365 Copilot, fullscreen is the only alternate mode supported.
Picture-in-picture
An ongoing activity that should stay visible while the conversation continues —
a live session, game, or video.
- Minimal controls. PiP overloaded with chrome is unusable at that size.
- Must react to conversation input, not sit static.
- Auto-closes when the session ends. Do not fight it.
Requesting a mode
const modes = app.getHostContext()?.availableDisplayModes ?? ["inline"];
if (modes.includes("fullscreen") && app.requestDisplayMode) {
await app.requestDisplayMode({ mode: "fullscreen" });
}
The host may grant a different mode than requested. Render from the mode you
were given, never the one you asked for.
Visual design
Color
- Use system colors for text, icons, and structural elements like dividers.
- Brand accents belong on logos, small icons, and the primary button — nothing else.
- Do not override system text color or background with brand color.
- No custom gradients or background overrides.
- Honor
hostContext.theme (and prefers-color-scheme as a backstop, since
onhostcontextchanged is not universally supported).
Typography
- Inherit the host's font stack. No custom fonts, even in fullscreen.
- Respect device-native sizing rules — SF Pro on iOS, sans-serif on Android.
- Limit size variation; prefer body and body-small.
- Partner styling (bold, italic, highlight) only within content areas.
Layout
- Use the host's grid spacing for cards and collections.
- Consistent padding; never edge-to-edge text.
- Respect
safeAreaInsets from host context.
- Drive height with
sendSizeChanged / notifyIntrinsicHeight rather than
guessing a fixed height.
Icons and imagery
- Monochromatic, outlined iconography that matches the host's language.
- Fixed aspect ratios; never distort.
- Do not render your own logo as a response element — the host appends
attribution itself.
Accessibility — non-negotiable
- WCAG AA contrast minimum for text against background.
- Alt text on every image.
- Text resizing must not break layout — no fixed pixel heights around text.
- Full keyboard reachability; visible focus states.
- Semantic elements and ARIA roles (
aria-pressed, aria-live for async
results). A2UI: plumb AccessibilityAttributes to the native API.
- Respect
prefers-reduced-motion.
Loading, empty, and error states
Every widget needs four states, and three of them get skipped.
| State |
Rule |
| Loading |
Render a skeleton from ontoolinput before the result arrives. Never a bare spinner when you already know the shape. |
| Empty |
Say what is missing and what to do next. Not "No results." |
| Error |
Human-readable message plus a retry path. Also call updateModelContext so the model knows the view is degraded and can help. |
| Partial |
With ontoolinputpartial, render progressively — but treat healed JSON as incomplete and never act on it. |
Interaction with the conversation
- The widget and the chat are one experience. Do not build a parallel chat.
- Use
ui/message / sendFollowUpMessage when the user should say something
next; use ui/update-model-context when the model just needs to know
something. Confusing these produces either a spammy transcript or a model
that has no idea what the user selected.
- One meaningful update per user action. Do not stream context on every keystroke.
Localization
The host mirrors locale to document.documentElement.lang.
const locale = document.documentElement.lang || "en-US";
Format dates, numbers, and currency from that locale — not from the server's.
Checklists
Do
- Inline for quick confirmations and simple actions
- Keep the composer usable in fullscreen
- Brand color on accents and badges only
- One clear CTA per carousel item
- Feature-detect before rendering any host-dependent affordance
Don't
- Deep navigation, tabs, or drill-ins inside a card
- Nested scrolling or cramped layouts
- Replicate host-native features
- Override system text colors or backgrounds
- Custom fonts, anywhere
- Overload PiP with controls or leave it static
Related
host-capability-matrix — which modes and APIs the target host supports.
ui-state-architecture — preserving presentation state across remounts.
ui-performance — skeletons, partial input, and offscreen pausing.
1---
2name: widget-ux-patterns
3description: This skill should be used when designing the visual and interaction layer of an agent-rendered widget — choosing among inline card, carousel, fullscreen, and picture-in-picture, action budgets, layout and typography rules, host theming, accessibility requirements, and the vendor dos and donts.
4version: 1.0.0
5trigger_phrases: [widget design, inline card, inline carousel, fullscreen widget, picture in picture, display mode, widget accessibility, widget theming, apps sdk design guidelines]
6categories: [design, ux, ui, reference]
7author: mcp-apps-studio
8created: 2026-08-13
9updated: 2026-08-13
10---
11
12# Widget UX patterns
13
14An agent widget is a guest in someone else's interface. It inherits their theme,
15competes with their composer, and gets closed the moment it feels like a
16different app. Design to the container.
17
18**Start with the smallest presentation that lets the user understand the result
19or finish the task.** Request more space only when the workflow demands it.
20
21## The four display modes
22
23### Inline card
24
25A focused result, a confirmation, or a small set of actions. The default.
26
27- **Maximum two primary actions** — one primary CTA, one optional secondary.
28- No deep navigation, no tabs, no drill-ins inside the card.
29- No nested scrolling. Auto-fit content to the available height.
30- Never duplicate a feature the host already provides.
31- Preserve visual hierarchy: headline → supporting text → CTA.
32
33Mobile: the card may expand its height to match content, up to the height of the
34mobile display area.
35
36### Inline carousel
37
38Scanning a small set of similar, visually rich options — restaurants, playlists,
39events.
40
41- **3–8 items.** Fewer looks broken; more is a list, not a carousel.
42- **≤3 lines of metadata** per item.
43- **One clear CTA** per item.
44- Consistent aspect ratios. A ragged carousel reads as a bug.
45
46### Fullscreen
47
48Rich tasks needing room: maps, editing canvases, detailed browsing.
49
50- The host's composer **stays overlaid**. Design around it — never place
51 controls where the composer lands.
52- Do not replicate a native app wholesale. Fullscreen is a focused surface, not
53 a port of your product.
54- Provide an obvious way back to inline.
55- In M365 Copilot, fullscreen is the *only* alternate mode supported.
56
57### Picture-in-picture
58
59An ongoing activity that should stay visible while the conversation continues —
60a live session, game, or video.
61
62- Minimal controls. PiP overloaded with chrome is unusable at that size.
63- Must react to conversation input, not sit static.
64- Auto-closes when the session ends. Do not fight it.
65
66### Requesting a mode
67
68```ts
69const modes = app.getHostContext()?.availableDisplayModes ?? ["inline"];
70if (modes.includes("fullscreen") && app.requestDisplayMode) {
71 await app.requestDisplayMode({ mode: "fullscreen" });
72}
73```
74
75The host may grant a different mode than requested. Render from the mode you
76were *given*, never the one you asked for.
77
78## Visual design
79
80### Color
81
82- Use **system colors** for text, icons, and structural elements like dividers.
83- Brand accents belong on logos, small icons, and the primary button — nothing else.
84- Do not override system text color or background with brand color.
85- No custom gradients or background overrides.
86- Honor `hostContext.theme` (and `prefers-color-scheme` as a backstop, since
87 `onhostcontextchanged` is not universally supported).
88
89### Typography
90
91- Inherit the host's font stack. **No custom fonts, even in fullscreen.**
92- Respect device-native sizing rules — SF Pro on iOS, sans-serif on Android.
93- Limit size variation; prefer body and body-small.
94- Partner styling (bold, italic, highlight) only *within* content areas.
95
96### Layout
97
98- Use the host's grid spacing for cards and collections.
99- Consistent padding; never edge-to-edge text.
100- Respect `safeAreaInsets` from host context.
101- Drive height with `sendSizeChanged` / `notifyIntrinsicHeight` rather than
102 guessing a fixed height.
103
104### Icons and imagery
105
106- Monochromatic, outlined iconography that matches the host's language.
107- Fixed aspect ratios; never distort.
108- **Do not render your own logo as a response element** — the host appends
109 attribution itself.
110
111## Accessibility — non-negotiable
112
113- WCAG AA contrast minimum for text against background.
114- Alt text on every image.
115- Text resizing must not break layout — no fixed pixel heights around text.
116- Full keyboard reachability; visible focus states.
117- Semantic elements and ARIA roles (`aria-pressed`, `aria-live` for async
118 results). A2UI: plumb `AccessibilityAttributes` to the native API.
119- Respect `prefers-reduced-motion`.
120
121## Loading, empty, and error states
122
123Every widget needs four states, and three of them get skipped.
124
125| State | Rule |
126|---|---|
127| **Loading** | Render a skeleton from `ontoolinput` before the result arrives. Never a bare spinner when you already know the shape. |
128| **Empty** | Say what is missing and what to do next. Not "No results." |
129| **Error** | Human-readable message plus a retry path. Also call `updateModelContext` so the model knows the view is degraded and can help. |
130| **Partial** | With `ontoolinputpartial`, render progressively — but treat healed JSON as incomplete and never act on it. |
131
132## Interaction with the conversation
133
134- The widget and the chat are one experience. Do not build a parallel chat.
135- Use `ui/message` / `sendFollowUpMessage` when the *user* should say something
136 next; use `ui/update-model-context` when the model just needs to *know*
137 something. Confusing these produces either a spammy transcript or a model
138 that has no idea what the user selected.
139- One meaningful update per user action. Do not stream context on every keystroke.
140
141## Localization
142
143The host mirrors locale to `document.documentElement.lang`.
144
145```tsx
146const locale = document.documentElement.lang || "en-US";
147```
148
149Format dates, numbers, and currency from that locale — not from the server's.
150
151## Checklists
152
153**Do**
154- Inline for quick confirmations and simple actions
155- Keep the composer usable in fullscreen
156- Brand color on accents and badges only
157- One clear CTA per carousel item
158- Feature-detect before rendering any host-dependent affordance
159
160**Don't**
161- Deep navigation, tabs, or drill-ins inside a card
162- Nested scrolling or cramped layouts
163- Replicate host-native features
164- Override system text colors or backgrounds
165- Custom fonts, anywhere
166- Overload PiP with controls or leave it static
167
168## Related
169
170- `host-capability-matrix` — which modes and APIs the target host supports.
171- `ui-state-architecture` — preserving presentation state across remounts.
172- `ui-performance` — skeletons, partial input, and offscreen pausing.