Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive guide for building production-ready MCP servers with tools, resources, prompts, and React widgets using mcp-use.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/widgets/ui-guidelines.md
1# Widget UI Guidelines23Build widgets that adapt to themes, look professional, and provide great user experience.45**Key topics:** Theme support, light/dark mode, responsive layouts, accessibility, CSS best practices67---89## Theme Support with useWidgetTheme()1011Widgets should adapt to the user's theme (light/dark mode):1213```tsx14import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";15import { z } from "zod";1617export const widgetMetadata: WidgetMetadata = {18description: "Theme-aware widget",19props: z.object({20message: z.string()21}),22exposeAsTool: false23};2425export default function ThemedWidget() {26const { props, isPending } = useWidget();27const theme = useWidgetTheme();2829if (isPending) {30return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;31}3233return (34<McpUseProvider autoSize>35<div style={{36padding: 20,37backgroundColor: theme === "dark" ? "#1e1e1e" : "#ffffff",38color: theme === "dark" ? "#ffffff" : "#000000"39}}>40<p>{props.message}</p>41</div>42</McpUseProvider>43);44}45```4647**useWidgetTheme() returns:** `"light"` or `"dark"`4849---5051## Theme-Aware Colors5253Define color palettes for both themes:5455```tsx56const theme = useWidgetTheme();5758const colors = {59background: theme === "dark" ? "#1e1e1e" : "#ffffff",60text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",61border: theme === "dark" ? "#404040" : "#e0e0e0",62primary: theme === "dark" ? "#4a9eff" : "#0066cc",63secondary: theme === "dark" ? "#6c757d" : "#6c757d",64hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",65error: theme === "dark" ? "#ff6b6b" : "#dc3545",66success: theme === "dark" ? "#51cf66" : "#28a745"67};6869return (70<McpUseProvider autoSize>71<div style={{72backgroundColor: colors.background,73color: colors.text,74border: `1px solid ${colors.border}`75}}>76{/* Your content */}77</div>78</McpUseProvider>79);80```8182Or extract to a hook:8384```tsx85function useColors() {86const theme = useWidgetTheme();8788return {89background: theme === "dark" ? "#1e1e1e" : "#ffffff",90text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",91border: theme === "dark" ? "#404040" : "#e0e0e0",92primary: theme === "dark" ? "#4a9eff" : "#0066cc",93hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",94error: theme === "dark" ? "#ff6b6b" : "#dc3545"95};96}9798export default function ThemedWidget() {99const colors = useColors();100// ... rest of component101}102```103104---105106## Responsive Layouts107108### Grid Layout109110```tsx111<div style={{112display: "grid",113gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",114gap: 16,115padding: 20116}}>117{props.items.map(item => (118<div key={item.id} style={{119padding: 12,120border: `1px solid ${colors.border}`,121borderRadius: 8122}}>123{item.name}124</div>125))}126</div>127```128129### Flexbox Layout130131```tsx132<div style={{133display: "flex",134gap: 16,135padding: 20,136flexWrap: "wrap"137}}>138{props.items.map(item => (139<div key={item.id} style={{140flex: "1 1 200px",141padding: 12,142border: `1px solid ${colors.border}`143}}>144{item.name}145</div>146))}147</div>148```149150### Two-Column Layout151152```tsx153<div style={{154display: "flex",155gap: 16,156padding: 20157}}>158{/* Sidebar */}159<div style={{ flex: "0 0 250px" }}>160{/* Navigation or filters */}161</div>162163{/* Main content */}164<div style={{ flex: 1 }}>165{/* Primary content */}166</div>167</div>168```169170---171172## Button Styles173174Theme-aware buttons:175176```tsx177const theme = useWidgetTheme();178179const buttonStyle: React.CSSProperties = {180padding: "8px 16px",181border: "none",182borderRadius: 4,183cursor: "pointer",184fontSize: 14,185fontWeight: 500,186backgroundColor: theme === "dark" ? "#4a9eff" : "#0066cc",187color: "#ffffff"188};189190const secondaryButtonStyle: React.CSSProperties = {191...buttonStyle,192backgroundColor: "transparent",193border: `1px solid ${theme === "dark" ? "#404040" : "#e0e0e0"}`,194color: theme === "dark" ? "#e0e0e0" : "#1a1a1a"195};196197return (198<McpUseProvider autoSize>199<div>200<button style={buttonStyle}>Primary Action</button>201<button style={secondaryButtonStyle}>Secondary</button>202</div>203</McpUseProvider>204);205```206207### Button States208209```tsx210const [hovered, setHovered] = useState(false);211212<button213style={{214padding: "8px 16px",215backgroundColor: hovered ? (theme === "dark" ? "#5aa8ff" : "#0052a3") : (theme === "dark" ? "#4a9eff" : "#0066cc"),216color: "#ffffff",217border: "none",218borderRadius: 4,219cursor: "pointer",220transition: "background-color 0.2s"221}}222onMouseEnter={() => setHovered(true)}223onMouseLeave={() => setHovered(false)}224>225Hover Me226</button>227```228229---230231## Card Components232233```tsx234const theme = useWidgetTheme();235236const cardStyle: React.CSSProperties = {237padding: 16,238border: `1px solid ${theme === "dark" ? "#404040" : "#e0e0e0"}`,239borderRadius: 8,240backgroundColor: theme === "dark" ? "#1e1e1e" : "#ffffff",241color: theme === "dark" ? "#e0e0e0" : "#1a1a1a"242};243244return (245<McpUseProvider autoSize>246<div style={{ padding: 20 }}>247{props.items.map(item => (248<div key={item.id} style={{249...cardStyle,250marginBottom: 12251}}>252<h3 style={{ margin: "0 0 8px 0" }}>{item.title}</h3>253<p style={{ margin: 0, color: theme === "dark" ? "#b0b0b0" : "#666" }}>254{item.description}255</p>256</div>257))}258</div>259</McpUseProvider>260);261```262263---264265## Typography266267```tsx268const theme = useWidgetTheme();269270<div style={{ padding: 20 }}>271{/* Heading */}272<h1 style={{273fontSize: 24,274fontWeight: 600,275margin: "0 0 16px 0",276color: theme === "dark" ? "#ffffff" : "#1a1a1a"277}}>278Title279</h1>280281{/* Subheading */}282<h2 style={{283fontSize: 18,284fontWeight: 500,285margin: "0 0 12px 0",286color: theme === "dark" ? "#e0e0e0" : "#333"287}}>288Subtitle289</h2>290291{/* Body text */}292<p style={{293fontSize: 14,294lineHeight: 1.5,295margin: "0 0 12px 0",296color: theme === "dark" ? "#b0b0b0" : "#666"297}}>298Body content here299</p>300301{/* Small text */}302<span style={{303fontSize: 12,304color: theme === "dark" ? "#808080" : "#999"305}}>306Small text or metadata307</span>308</div>309```310311---312313## Form Inputs314315```tsx316const theme = useWidgetTheme();317318const inputStyle: React.CSSProperties = {319padding: 8,320fontSize: 14,321border: `1px solid ${theme === "dark" ? "#404040" : "#d0d0d0"}`,322borderRadius: 4,323backgroundColor: theme === "dark" ? "#2a2a2a" : "#ffffff",324color: theme === "dark" ? "#e0e0e0" : "#1a1a1a",325outline: "none"326};327328<form style={{ padding: 20 }}>329<label style={{330display: "block",331marginBottom: 4,332fontSize: 14,333fontWeight: 500,334color: theme === "dark" ? "#e0e0e0" : "#333"335}}>336Name337</label>338<input339type="text"340style={inputStyle}341placeholder="Enter name..."342/>343344<label style={{ display: "block", marginTop: 12, marginBottom: 4 }}>345Description346</label>347<textarea348style={{349...inputStyle,350width: "100%",351minHeight: 80,352resize: "vertical"353}}354placeholder="Enter description..."355/>356</form>357```358359---360361## Lists362363```tsx364const theme = useWidgetTheme();365366<ul style={{367listStyle: "none",368padding: 0,369margin: 0370}}>371{props.items.map(item => (372<li373key={item.id}374style={{375padding: 12,376borderBottom: `1px solid ${theme === "dark" ? "#2a2a2a" : "#f0f0f0"}`,377cursor: "pointer",378transition: "background-color 0.15s"379}}380onMouseEnter={(e) => {381e.currentTarget.style.backgroundColor = theme === "dark" ? "#2a2a2a" : "#f5f5f5";382}}383onMouseLeave={(e) => {384e.currentTarget.style.backgroundColor = "transparent";385}}386>387{item.name}388</li>389))}390</ul>391```392393---394395## Badges and Tags396397```tsx398const theme = useWidgetTheme();399400const badgeStyle: React.CSSProperties = {401display: "inline-block",402padding: "4px 8px",403fontSize: 12,404fontWeight: 500,405borderRadius: 12,406backgroundColor: theme === "dark" ? "#2a4a6a" : "#e3f2fd",407color: theme === "dark" ? "#4a9eff" : "#0066cc"408};409410<div>411<span style={badgeStyle}>New</span>412<span style={{ ...badgeStyle, marginLeft: 8 }}>Featured</span>413</div>414```415416---417418## Loading States419420```tsx421const theme = useWidgetTheme();422423if (isPending) {424return (425<McpUseProvider autoSize>426<div style={{427padding: 40,428textAlign: "center",429color: theme === "dark" ? "#808080" : "#999"430}}>431<div style={{432width: 40,433height: 40,434border: `4px solid ${theme === "dark" ? "#404040" : "#e0e0e0"}`,435borderTop: `4px solid ${theme === "dark" ? "#4a9eff" : "#0066cc"}`,436borderRadius: "50%",437margin: "0 auto 16px",438animation: "spin 1s linear infinite"439}} />440<p>Loading...</p>441</div>442</McpUseProvider>443);444}445```446447Add spin animation:448449```tsx450<style>451{`452@keyframes spin {4530% { transform: rotate(0deg); }454100% { transform: rotate(360deg); }455}456`}457</style>458```459460---461462## Empty States463464```tsx465const theme = useWidgetTheme();466467{props.items.length === 0 && (468<div style={{469padding: 40,470textAlign: "center",471color: theme === "dark" ? "#808080" : "#999"472}}>473<div style={{474fontSize: 48,475marginBottom: 16,476opacity: 0.5477}}>478๐ญ479</div>480<h3 style={{481fontSize: 18,482fontWeight: 500,483margin: "0 0 8px 0",484color: theme === "dark" ? "#b0b0b0" : "#666"485}}>486No items yet487</h3>488<p style={{489fontSize: 14,490margin: 0,491color: theme === "dark" ? "#808080" : "#999"492}}>493Get started by creating your first item494</p>495</div>496)}497```498499---500501## Error States502503```tsx504const theme = useWidgetTheme();505506{error && (507<div style={{508padding: 12,509marginBottom: 16,510backgroundColor: theme === "dark" ? "#3d1f1f" : "#ffebee",511color: theme === "dark" ? "#ff6b6b" : "#c62828",512border: `1px solid ${theme === "dark" ? "#6b2a2a" : "#ffcdd2"}`,513borderRadius: 4514}}>515<strong>Error:</strong> {error}516</div>517)}518```519520---521522## Icons523524Use Unicode emojis or SVG icons:525526```tsx527// Emojis528<span style={{ fontSize: 24, marginRight: 8 }}>โ๏ธ</span>529<span style={{ fontSize: 20 }}>โ</span>530<span>โ</span>531532// SVG icon533<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">534<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z"/>535</svg>536```537538---539540## Spacing Guidelines541542```tsx543// Consistent spacing units544const spacing = {545xs: 4,546sm: 8,547md: 12,548lg: 16,549xl: 20,550xxl: 24551};552553<div style={{554padding: spacing.lg,555gap: spacing.md556}}>557{/* Content */}558</div>559```560561---562563## Accessibility564565### Labels for Inputs566```tsx567<label htmlFor="email-input">Email</label>568<input id="email-input" type="email" />569```570571### Alt Text for Images572```tsx573<img src={item.image} alt={item.name} />574```575576### Button Labels577```tsx578<button aria-label="Delete item">๐๏ธ</button>579```580581### Keyboard Navigation582```tsx583<div584tabIndex={0}585onKeyDown={(e) => {586if (e.key === "Enter" || e.key === " ") {587handleClick();588}589}}590>591Clickable item592</div>593```594595---596597## Auto-Size Best Practices598599`<McpUseProvider autoSize>` automatically resizes iframe to content.600601**Tips:**602- Use `autoSize` for dynamic content603- Avoid fixed heights unless necessary604- Widget resizes when content changes605- Test with varying content sizes606607```tsx608// โ Good - autoSize handles height609<McpUseProvider autoSize>610<div style={{ padding: 20 }}>611{/* Dynamic content */}612</div>613</McpUseProvider>614615// โ Bad - Fixed height defeats autoSize616<McpUseProvider autoSize>617<div style={{ height: 400, overflow: "auto" }}>618{/* Content */}619</div>620</McpUseProvider>621```622623---624625## Complete Themed Widget626627```tsx628import { useState } from "react";629import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";630import { z } from "zod";631632function useColors() {633const theme = useWidgetTheme();634635return {636background: theme === "dark" ? "#1e1e1e" : "#ffffff",637text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",638textSecondary: theme === "dark" ? "#b0b0b0" : "#666",639border: theme === "dark" ? "#404040" : "#e0e0e0",640hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",641primary: theme === "dark" ? "#4a9eff" : "#0066cc"642};643}644645export const widgetMetadata: WidgetMetadata = {646description: "Fully themed product list",647props: z.object({648products: z.array(z.object({649id: z.string(),650name: z.string(),651price: z.number(),652category: z.string()653}))654}),655exposeAsTool: false656};657658export default function ThemedProductList() {659const { props, isPending } = useWidget();660const colors = useColors();661const [selectedCategory, setSelectedCategory] = useState("all");662663if (isPending) {664return (665<McpUseProvider autoSize>666<div style={{667padding: 40,668textAlign: "center",669color: colors.textSecondary670}}>671Loading...672</div>673</McpUseProvider>674);675}676677const categories = ["all", ...new Set(props.products.map(p => p.category))];678const filtered = selectedCategory === "all"679? props.products680: props.products.filter(p => p.category === selectedCategory);681682return (683<McpUseProvider autoSize>684<div style={{685padding: 20,686backgroundColor: colors.background,687color: colors.text688}}>689<h2 style={{ margin: "0 0 16px 0" }}>Products</h2>690691{/* Category filters */}692<div style={{ marginBottom: 16, display: "flex", gap: 8 }}>693{categories.map(cat => (694<button695key={cat}696onClick={() => setSelectedCategory(cat)}697style={{698padding: "8px 16px",699borderRadius: 4,700cursor: "pointer",701backgroundColor: selectedCategory === cat ? colors.primary : "transparent",702color: selectedCategory === cat ? "#fff" : colors.text,703border: `1px solid ${selectedCategory === cat ? colors.primary : colors.border}`704}}705>706{cat}707</button>708))}709</div>710711{/* Product grid */}712<div style={{713display: "grid",714gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",715gap: 12716}}>717{filtered.map(product => (718<div719key={product.id}720style={{721padding: 12,722border: `1px solid ${colors.border}`,723borderRadius: 8,724backgroundColor: colors.background725}}726>727<h3 style={{ margin: "0 0 4px 0", fontSize: 16 }}>728{product.name}729</h3>730<p style={{ margin: "0 0 8px 0", fontSize: 12, color: colors.textSecondary }}>731{product.category}732</p>733<p style={{ margin: 0, fontSize: 18, fontWeight: "bold", color: colors.primary }}>734${product.price}735</p>736</div>737))}738</div>739740{filtered.length === 0 && (741<div style={{742padding: 40,743textAlign: "center",744color: colors.textSecondary745}}>746No products in this category747</div>748)}749</div>750</McpUseProvider>751);752}753```754755---756757## Next Steps758759- **Advanced patterns** โ [advanced.md](advanced.md)760- **See examples** โ [../patterns/common-patterns.md](../patterns/common-patterns.md)761