0

Widget UI Guidelines

awesome-llm-appswidgets

Build widgets that adapt to themes, look professional, and provide great user experience.

Sign in to save downloads to your library and vote.

Preview

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 autoSize for 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

Ingestion metadata

Source catalog
awesome-llm-apps
Repository
Shubhamsaboo/awesome-llm-apps ยท main
File path
generative_ui_agents/ai-mcp-app-builder/apps/mcp-use-server/.agent/skills/mcp-apps-builder/references/widgets/ui-guidelines.md
Last refreshed
10/24/2026, 3:00:14 AM (5h ago)
Refresh schedule
Daily ยท 03:00 UTC
Dedupe status
Unique ยท deduped by (source, url)