Widget Rendering
Next.js widget implementation and Apps SDK integration for ChatGPT
Widget Implementation Guide
Table of Contents
- Overview - Widget execution model
- Required Patterns - Must-implement patterns
- Display Mode Handling - Layout decisions by mode
- Safe Area Handling - Mobile layout constraints
- Widget State Persistence - Cross-tool-call state
- Platform Constraints - ChatGPT-specific rules
- Cross-System Integration - How widgets relate to MCP tools
- References - Implementation examples
Overview
Widgets are Next.js pages rendered inside ChatGPT iframes when MCP tools return _meta.widget in their response.
Critical behavior: ChatGPT controls widget lifecycle:
- Tool executes → ChatGPT renders widget with loading state → Tool completes → Widget receives data
- Tool throws error → ChatGPT hides widget (no error UI needed in widget)
- User invokes another tool → Widget state persists if using
useWidgetState()
Primary widget: app/artifact/page.tsx (renders artifacts returned by artifact.view and artifact.create tools)
Required Patterns
1. Loading State Pattern
MUST implement: Widget pages must handle the loading state while tools execute.
// app/artifact/page.tsx pattern
export default function ArtifactWidgetPage() {
const toolOutput = useWidgetProps<ArtifactToolOutput>({})
const artifact = toolOutput?.artifact
const metadata = toolOutput?.metadata
// REQUIRED: Handle loading state (toolOutput is null while tool executes)
if (!artifact || !metadata) {
return <LoadingUI />
}
// Success state: render artifact
return <ArtifactContent artifact={artifact} metadata={metadata} />
}Why required:
window.openai.toolOutputisnullwhile tool executes- Widget must show something during execution or appears broken
- ChatGPT handles errors automatically (widget hidden on tool throw)
2. Browser API Patches
MUST NOT bypass: app/layout.tsx contains NextChatSDKBootstrap component that patches browser APIs for iframe compatibility.
Patched APIs:
history.pushState/replaceState- Client-side navigationwindow.fetch- CORS handling- External link clicks - Routed through
window.openai.openExternal()
Constraint: Write normal Next.js code. The patches make standard patterns work in ChatGPT's iframe. Don't try to "work around" them.
Display Mode Handling
ChatGPT renders widgets in three modes. Widget must handle layout differences.
Display Modes
| Mode | Use Case | Layout Constraints |
|---|---|---|
inline | Default card in conversation | Limited height (~400-600px), show preview/summary |
fullscreen | User expands widget | Full viewport, show complete UI with details |
pip | User pins while chatting | Fixed overlay, minimal interactions |
Decision Criteria
When to differentiate:
const displayMode = useDisplayMode()
if (displayMode === 'inline') {
// Show compact preview with "Expand fullscreen" button
return <CompactCard />
}
// fullscreen or pip
return <DetailedView />Pattern in artifact widget (app/artifact/page.tsx:139-179):
inline: Shows card with metadata, "Open fullscreen" button, no artifact renderingfullscreen: Shows full artifact with header, renders complete<Artifact>component
Common mistake: Rendering full content in inline mode causes scroll issues. Use inline for preview only.
Safe Area Handling
When needed: Fullscreen mode on mobile devices with notches/home indicators.
Pattern:
const safeArea = useSafeArea()
const displayMode = useDisplayMode()
// Apply padding in fullscreen to avoid system UI overlap
<div style={{
paddingTop: displayMode === 'fullscreen' ? safeArea?.insets.top : 0,
paddingBottom: displayMode === 'fullscreen' ? safeArea?.insets.bottom : 0,
paddingLeft: displayMode === 'fullscreen' ? safeArea?.insets.left : 0,
paddingRight: displayMode === 'fullscreen' ? safeArea?.insets.right : 0,
}}>Note structure: safeArea.insets.bottom (nested), not safeArea.bottom
When to skip: Inline and pip modes don't need safe area handling.
Widget State Persistence
Use case: Preserve UI state when user invokes another tool while widget is visible.
const [state, setState] = useWidgetState<{
selectedTab?: string
expandedSections: string[]
}>({
expandedSections: [] // default
})
// State persists across tool invocations
// Model can see state in window.openai.widgetStateWhen to use:
- Multi-step workflows where widget stays open
- User interactions that should persist (tab selection, filters)
When to skip:
- Simple view-only widgets
- State that should reset on each tool call
Platform Constraints
Must Follow
- Loading state required: Widget shows while tool executes, must handle
toolOutput === null - No error UI needed: ChatGPT hides widget when tools throw errors
- Display mode awareness: Differentiate inline (preview) from fullscreen (full UI)
- Safe area in fullscreen: Apply padding on mobile to avoid notch/home indicator overlap
Must Not Do
- Don't bypass patched APIs: Use standard
fetch,history, link clicks - patches make them work - Don't render full content in inline: Causes scroll issues, use preview only
- Don't expect toolOutput immediately: It's null during tool execution
Cross-System Integration
Widget ↔ MCP Tool Flow
1. MCP tool handler (app/mcp/tools/artifact-view.ts) executes
↓
2. Returns { content, structuredContent, _meta: { widget: { ... } } }
↓
3. ChatGPT renders widget iframe loading app/artifact page
↓
4. Widget shows loading UI (toolOutput is null)
↓
5. Tool completes, toolOutput populated
↓
6. Widget re-renders with dataKey insight: Widget HTML is pre-loaded by MCP server at startup (see mcp-server.md "Widget HTML Loading"). The HTML is cached, not fetched per-request.
Widget ↔ Authentication
Widgets receive tool output but don't directly handle auth:
- Auth happens in MCP tool handler (see authentication.md)
- Tool filters data by userId before returning
- Widget receives only user's authorized data
References
Working Implementations
app/artifact/page.tsx- Complete widget with loading state, display modes, safe areaapp/layout.tsx- NextChatSDKBootstrap browser API patchesapp/hooks/- ChatGPT integration hooks (useWidgetProps, useDisplayMode, etc.)
Related Documentation
- MCP Server - How tools return widget metadata
- Authentication - OAuth flow (happens in MCP tools, not widgets)
Upstream Reference
docs/cgpt-apps-sdk/- ChatGPT Apps SDK platform documentation (generic patterns)