Loading...
Loading...
Best practices for designing UI widgets in xmcp. Use when creating interactive widgets for GPT Apps or MCP Apps, choosing between React components and template literals, designing widget layouts, handling state and data fetching, or troubleshooting widget rendering issues.
npx skill4agent add xmcp-dev/skills widget-design_meta.openaiwidgetAccessible: true| Scenario | Handler | Reason |
|---|---|---|
| User interaction needed (buttons, inputs) | React ( | State management with hooks |
| Display external widget library | Template literal | Just load scripts/styles |
| Dynamic content from tool params | React | Props flow naturally |
| Static HTML with no state | Template literal | Simpler, less overhead |
// Bad: Requires click to see anything
export default function Widget() {
const [data, setData] = useState(null);
return <button onClick={fetchData}>Load Data</button>;
}
// Good: Shows data immediately
export default function Widget({ query }) {
const [data, setData] = useState(null);
useEffect(() => { fetchData(query).then(setData); }, [query]);
return data ? <Results data={data} /> : <Loading />;
}<div className="space-y-4">
{/* Label: small, muted, uppercase */}
<div className="text-sm text-zinc-500 uppercase tracking-wider">Temperature</div>
{/* Value: large, prominent */}
<div className="text-5xl font-light">72°F</div>
{/* Supporting: medium, secondary */}
<div className="text-zinc-400">Feels like 68°F</div>
</div>// Bad: No visual feedback
<button className="px-4 py-2 bg-white/10">Click</button>
// Good: Hover + transition
<button className="px-4 py-2 bg-white/10 hover:bg-white/20 border border-white/10 hover:border-white/20 transition-all duration-200">
Click
</button>// Good: Local state with hooks
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);export default function Widget() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchData()
.then(setData)
.catch(e => setError(e.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <Loading />;
if (error) return <Error message={error} />;
return <Display data={data} />;
}// Bad: User must click to see data
<button onClick={() => fetchData()}>Load</button>
// Good: Fetch automatically
useEffect(() => { fetchData(); }, []);// Broken
_meta: { openai: { toolInvocation: { ... } } }
// Fixed
_meta: { openai: { widgetAccessible: true, toolInvocation: { ... } } }// Broken: fetch fails silently
fetch('https://api.weather.com/data');
// Fixed: declare in metadata
_meta: {
openai: {
widgetCSP: { connect_domains: ["https://api.weather.com"] }
}
}// Bad
<div style={{ width: '800px' }}>...</div>
// Good
<div className="w-full max-w-2xl mx-auto">...</div>// Always wrap risky operations
try {
const result = JSON.parse(data);
} catch {
return <Error message="Invalid data format" />;
}return {
structuredContent: { game: "doom", url: "..." },
content: [{ type: "text", text: "Launching DOOM..." }],
};useToolOutput()// This is enough for MCP Apps
export const metadata = { name: "widget", description: "..." };
export default function Widget() { return <div>Hello</div>; }widgetAccessible: truetoolInvocation