Widget UI Guidelines
Build widgets that adapt to themes, look professional, and provide great user experience.
Key topics: Theme support, light/dark mode, responsive layouts, accessibility, CSS best practices
Theme Support with useWidgetTheme()
Widgets should adapt to the user's theme (light/dark mode):
import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
export const widgetMetadata: WidgetMetadata = {
description: "Theme-aware widget",
props: z.object({
message: z.string()
}),
exposeAsTool: false
};
export default function ThemedWidget() {
const { props, isPending } = useWidget();
const theme = useWidgetTheme();
if (isPending) {
return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
}
return (
<McpUseProvider autoSize>
<div style={{
padding: 20,
backgroundColor: theme === "dark" ? "#1e1e1e" : "#ffffff",
color: theme === "dark" ? "#ffffff" : "#000000"
}}>
<p>{props.message}</p>
</div>
</McpUseProvider>
);
}useWidgetTheme() returns: "light" or "dark"
Theme-Aware Colors
Define color palettes for both themes:
const theme = useWidgetTheme();
const colors = {
background: theme === "dark" ? "#1e1e1e" : "#ffffff",
text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
border: theme === "dark" ? "#404040" : "#e0e0e0",
primary: theme === "dark" ? "#4a9eff" : "#0066cc",
secondary: theme === "dark" ? "#6c757d" : "#6c757d",
hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",
error: theme === "dark" ? "#ff6b6b" : "#dc3545",
success: theme === "dark" ? "#51cf66" : "#28a745"
};
return (
<McpUseProvider autoSize>
<div style={{
backgroundColor: colors.background,
color: colors.text,
border: `1px solid ${colors.border}`
}}>
{/* Your content */}
</div>
</McpUseProvider>
);Or extract to a hook:
function useColors() {
const theme = useWidgetTheme();
return {
background: theme === "dark" ? "#1e1e1e" : "#ffffff",
text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
border: theme === "dark" ? "#404040" : "#e0e0e0",
primary: theme === "dark" ? "#4a9eff" : "#0066cc",
hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",
error: theme === "dark" ? "#ff6b6b" : "#dc3545"
};
}
export default function ThemedWidget() {
const colors = useColors();
// ... rest of component
}Responsive Layouts
Grid Layout
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: 16,
padding: 20
}}>
{props.items.map(item => (
<div key={item.id} style={{
padding: 12,
border: `1px solid ${colors.border}`,
borderRadius: 8
}}>
{item.name}
</div>
))}
</div>Flexbox Layout
<div style={{
display: "flex",
gap: 16,
padding: 20,
flexWrap: "wrap"
}}>
{props.items.map(item => (
<div key={item.id} style={{
flex: "1 1 200px",
padding: 12,
border: `1px solid ${colors.border}`
}}>
{item.name}
</div>
))}
</div>Two-Column Layout
<div style={{
display: "flex",
gap: 16,
padding: 20
}}>
{/* Sidebar */}
<div style={{ flex: "0 0 250px" }}>
{/* Navigation or filters */}
</div>
{/* Main content */}
<div style={{ flex: 1 }}>
{/* Primary content */}
</div>
</div>Button Styles
Theme-aware buttons:
const theme = useWidgetTheme();
const buttonStyle: React.CSSProperties = {
padding: "8px 16px",
border: "none",
borderRadius: 4,
cursor: "pointer",
fontSize: 14,
fontWeight: 500,
backgroundColor: theme === "dark" ? "#4a9eff" : "#0066cc",
color: "#ffffff"
};
const secondaryButtonStyle: React.CSSProperties = {
...buttonStyle,
backgroundColor: "transparent",
border: `1px solid ${theme === "dark" ? "#404040" : "#e0e0e0"}`,
color: theme === "dark" ? "#e0e0e0" : "#1a1a1a"
};
return (
<McpUseProvider autoSize>
<div>
<button style={buttonStyle}>Primary Action</button>
<button style={secondaryButtonStyle}>Secondary</button>
</div>
</McpUseProvider>
);Button States
const [hovered, setHovered] = useState(false);
<button
style={{
padding: "8px 16px",
backgroundColor: hovered ? (theme === "dark" ? "#5aa8ff" : "#0052a3") : (theme === "dark" ? "#4a9eff" : "#0066cc"),
color: "#ffffff",
border: "none",
borderRadius: 4,
cursor: "pointer",
transition: "background-color 0.2s"
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
Hover Me
</button>Card Components
const theme = useWidgetTheme();
const cardStyle: React.CSSProperties = {
padding: 16,
border: `1px solid ${theme === "dark" ? "#404040" : "#e0e0e0"}`,
borderRadius: 8,
backgroundColor: theme === "dark" ? "#1e1e1e" : "#ffffff",
color: theme === "dark" ? "#e0e0e0" : "#1a1a1a"
};
return (
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
{props.items.map(item => (
<div key={item.id} style={{
...cardStyle,
marginBottom: 12
}}>
<h3 style={{ margin: "0 0 8px 0" }}>{item.title}</h3>
<p style={{ margin: 0, color: theme === "dark" ? "#b0b0b0" : "#666" }}>
{item.description}
</p>
</div>
))}
</div>
</McpUseProvider>
);Typography
const theme = useWidgetTheme();
<div style={{ padding: 20 }}>
{/* Heading */}
<h1 style={{
fontSize: 24,
fontWeight: 600,
margin: "0 0 16px 0",
color: theme === "dark" ? "#ffffff" : "#1a1a1a"
}}>
Title
</h1>
{/* Subheading */}
<h2 style={{
fontSize: 18,
fontWeight: 500,
margin: "0 0 12px 0",
color: theme === "dark" ? "#e0e0e0" : "#333"
}}>
Subtitle
</h2>
{/* Body text */}
<p style={{
fontSize: 14,
lineHeight: 1.5,
margin: "0 0 12px 0",
color: theme === "dark" ? "#b0b0b0" : "#666"
}}>
Body content here
</p>
{/* Small text */}
<span style={{
fontSize: 12,
color: theme === "dark" ? "#808080" : "#999"
}}>
Small text or metadata
</span>
</div>Form Inputs
const theme = useWidgetTheme();
const inputStyle: React.CSSProperties = {
padding: 8,
fontSize: 14,
border: `1px solid ${theme === "dark" ? "#404040" : "#d0d0d0"}`,
borderRadius: 4,
backgroundColor: theme === "dark" ? "#2a2a2a" : "#ffffff",
color: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
outline: "none"
};
<form style={{ padding: 20 }}>
<label style={{
display: "block",
marginBottom: 4,
fontSize: 14,
fontWeight: 500,
color: theme === "dark" ? "#e0e0e0" : "#333"
}}>
Name
</label>
<input
type="text"
style={inputStyle}
placeholder="Enter name..."
/>
<label style={{ display: "block", marginTop: 12, marginBottom: 4 }}>
Description
</label>
<textarea
style={{
...inputStyle,
width: "100%",
minHeight: 80,
resize: "vertical"
}}
placeholder="Enter description..."
/>
</form>Lists
const theme = useWidgetTheme();
<ul style={{
listStyle: "none",
padding: 0,
margin: 0
}}>
{props.items.map(item => (
<li
key={item.id}
style={{
padding: 12,
borderBottom: `1px solid ${theme === "dark" ? "#2a2a2a" : "#f0f0f0"}`,
cursor: "pointer",
transition: "background-color 0.15s"
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = theme === "dark" ? "#2a2a2a" : "#f5f5f5";
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = "transparent";
}}
>
{item.name}
</li>
))}
</ul>Badges and Tags
const theme = useWidgetTheme();
const badgeStyle: React.CSSProperties = {
display: "inline-block",
padding: "4px 8px",
fontSize: 12,
fontWeight: 500,
borderRadius: 12,
backgroundColor: theme === "dark" ? "#2a4a6a" : "#e3f2fd",
color: theme === "dark" ? "#4a9eff" : "#0066cc"
};
<div>
<span style={badgeStyle}>New</span>
<span style={{ ...badgeStyle, marginLeft: 8 }}>Featured</span>
</div>Loading States
const theme = useWidgetTheme();
if (isPending) {
return (
<McpUseProvider autoSize>
<div style={{
padding: 40,
textAlign: "center",
color: theme === "dark" ? "#808080" : "#999"
}}>
<div style={{
width: 40,
height: 40,
border: `4px solid ${theme === "dark" ? "#404040" : "#e0e0e0"}`,
borderTop: `4px solid ${theme === "dark" ? "#4a9eff" : "#0066cc"}`,
borderRadius: "50%",
margin: "0 auto 16px",
animation: "spin 1s linear infinite"
}} />
<p>Loading...</p>
</div>
</McpUseProvider>
);
}Add spin animation:
<style>
{`
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`}
</style>Empty States
const theme = useWidgetTheme();
{props.items.length === 0 && (
<div style={{
padding: 40,
textAlign: "center",
color: theme === "dark" ? "#808080" : "#999"
}}>
<div style={{
fontSize: 48,
marginBottom: 16,
opacity: 0.5
}}>
๐ญ
</div>
<h3 style={{
fontSize: 18,
fontWeight: 500,
margin: "0 0 8px 0",
color: theme === "dark" ? "#b0b0b0" : "#666"
}}>
No items yet
</h3>
<p style={{
fontSize: 14,
margin: 0,
color: theme === "dark" ? "#808080" : "#999"
}}>
Get started by creating your first item
</p>
</div>
)}Error States
const theme = useWidgetTheme();
{error && (
<div style={{
padding: 12,
marginBottom: 16,
backgroundColor: theme === "dark" ? "#3d1f1f" : "#ffebee",
color: theme === "dark" ? "#ff6b6b" : "#c62828",
border: `1px solid ${theme === "dark" ? "#6b2a2a" : "#ffcdd2"}`,
borderRadius: 4
}}>
<strong>Error:</strong> {error}
</div>
)}Icons
Use Unicode emojis or SVG icons:
// Emojis
<span style={{ fontSize: 24, marginRight: 8 }}>โ๏ธ</span>
<span style={{ fontSize: 20 }}>โ</span>
<span>โ</span>
// SVG icon
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<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"/>
</svg>Spacing Guidelines
// Consistent spacing units
const spacing = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
xxl: 24
};
<div style={{
padding: spacing.lg,
gap: spacing.md
}}>
{/* Content */}
</div>Accessibility
Labels for Inputs
<label htmlFor="email-input">Email</label>
<input id="email-input" type="email" />Alt Text for Images
<img src={item.image} alt={item.name} />Button Labels
<button aria-label="Delete item">๐๏ธ</button>Keyboard Navigation
<div
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleClick();
}
}}
>
Clickable item
</div>Auto-Size Best Practices
<McpUseProvider autoSize> automatically resizes iframe to content.
Tips:
- Use
autoSizefor dynamic content - Avoid fixed heights unless necessary
- Widget resizes when content changes
- Test with varying content sizes
// โ
Good - autoSize handles height
<McpUseProvider autoSize>
<div style={{ padding: 20 }}>
{/* Dynamic content */}
</div>
</McpUseProvider>
// โ Bad - Fixed height defeats autoSize
<McpUseProvider autoSize>
<div style={{ height: 400, overflow: "auto" }}>
{/* Content */}
</div>
</McpUseProvider>Complete Themed Widget
import { useState } from "react";
import { McpUseProvider, useWidget, useWidgetTheme, type WidgetMetadata } from "mcp-use/react";
import { z } from "zod";
function useColors() {
const theme = useWidgetTheme();
return {
background: theme === "dark" ? "#1e1e1e" : "#ffffff",
text: theme === "dark" ? "#e0e0e0" : "#1a1a1a",
textSecondary: theme === "dark" ? "#b0b0b0" : "#666",
border: theme === "dark" ? "#404040" : "#e0e0e0",
hover: theme === "dark" ? "#2a2a2a" : "#f5f5f5",
primary: theme === "dark" ? "#4a9eff" : "#0066cc"
};
}
export const widgetMetadata: WidgetMetadata = {
description: "Fully themed product list",
props: z.object({
products: z.array(z.object({
id: z.string(),
name: z.string(),
price: z.number(),
category: z.string()
}))
}),
exposeAsTool: false
};
export default function ThemedProductList() {
const { props, isPending } = useWidget();
const colors = useColors();
const [selectedCategory, setSelectedCategory] = useState("all");
if (isPending) {
return (
<McpUseProvider autoSize>
<div style={{
padding: 40,
textAlign: "center",
color: colors.textSecondary
}}>
Loading...
</div>
</McpUseProvider>
);
}
const categories = ["all", ...new Set(props.products.map(p => p.category))];
const filtered = selectedCategory === "all"
? props.products
: props.products.filter(p => p.category === selectedCategory);
return (
<McpUseProvider autoSize>
<div style={{
padding: 20,
backgroundColor: colors.background,
color: colors.text
}}>
<h2 style={{ margin: "0 0 16px 0" }}>Products</h2>
{/* Category filters */}
<div style={{ marginBottom: 16, display: "flex", gap: 8 }}>
{categories.map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
style={{
padding: "8px 16px",
borderRadius: 4,
cursor: "pointer",
backgroundColor: selectedCategory === cat ? colors.primary : "transparent",
color: selectedCategory === cat ? "#fff" : colors.text,
border: `1px solid ${selectedCategory === cat ? colors.primary : colors.border}`
}}
>
{cat}
</button>
))}
</div>
{/* Product grid */}
<div style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
gap: 12
}}>
{filtered.map(product => (
<div
key={product.id}
style={{
padding: 12,
border: `1px solid ${colors.border}`,
borderRadius: 8,
backgroundColor: colors.background
}}
>
<h3 style={{ margin: "0 0 4px 0", fontSize: 16 }}>
{product.name}
</h3>
<p style={{ margin: "0 0 8px 0", fontSize: 12, color: colors.textSecondary }}>
{product.category}
</p>
<p style={{ margin: 0, fontSize: 18, fontWeight: "bold", color: colors.primary }}>
${product.price}
</p>
</div>
))}
</div>
{filtered.length === 0 && (
<div style={{
padding: 40,
textAlign: "center",
color: colors.textSecondary
}}>
No products in this category
</div>
)}
</div>
</McpUseProvider>
);
}Next Steps
- Advanced patterns โ advanced.md
- See examples โ ../patterns/common-patterns.md