Artifacts Platform
Agent DocsAppschatgpt-app

Widget Rendering

Next.js widget implementation and Apps SDK integration for ChatGPT

Widget Implementation Guide

Table of Contents


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.toolOutput is null while 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 navigation
  • window.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

ModeUse CaseLayout Constraints
inlineDefault card in conversationLimited height (~400-600px), show preview/summary
fullscreenUser expands widgetFull viewport, show complete UI with details
pipUser pins while chattingFixed 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 rendering
  • fullscreen: 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.widgetState

When 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

  1. Loading state required: Widget shows while tool executes, must handle toolOutput === null
  2. No error UI needed: ChatGPT hides widget when tools throw errors
  3. Display mode awareness: Differentiate inline (preview) from fullscreen (full UI)
  4. Safe area in fullscreen: Apply padding on mobile to avoid notch/home indicator overlap

Must Not Do

  1. Don't bypass patched APIs: Use standard fetch, history, link clicks - patches make them work
  2. Don't render full content in inline: Causes scroll issues, use preview only
  3. 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 data

Key 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 area
  • app/layout.tsx - NextChatSDKBootstrap browser API patches
  • app/hooks/ - ChatGPT integration hooks (useWidgetProps, useDisplayMode, etc.)

Upstream Reference

  • docs/cgpt-apps-sdk/ - ChatGPT Apps SDK platform documentation (generic patterns)

On this page

Widget Rendering