Skip to main content
An MCP App is a new kind of surface with its own UX principles. The view shares the screen with an ongoing conversation, in a frame the host controls. Skybridge surfaces that environment through hooks, so the view can read its context and adapt.
views/carousel.tsx
import { useDisplayMode, useLayout, useUser } from "skybridge/web";
import { useToolInfo } from "../helpers.js"; // generated, type-safe from server schema

export default function Carousel() {
  const { output } = useToolInfo<"search-products">();
  const { theme, maxHeight, safeArea } = useLayout();
  const [mode, setMode] = useDisplayMode();
  const { userAgent } = useUser();

  return (
    <div
      className={theme === "dark" ? "dark" : ""}
      style={{ maxHeight, paddingBottom: safeArea.insets.bottom }}
    >
      <ProductGrid
        products={output.products}
        columns={mode === "fullscreen" ? 4 : 2}
        compact={userAgent.device.type === "mobile"}
      />
      {mode === "inline" && (
        <button onClick={() => setMode("fullscreen")}>See all</button>
      )}
    </div>
  );
}
The sections below cover fitting the space the host gives you, matching its theme, responding to the display mode, and adapting to the user’s device.

Fit the Available Space

useLayout reports the room the host gives the view: maxHeight, and the safeArea insets that keep content clear of device notches and the chat composer.
views/carousel.tsx
import { useDisplayMode, useLayout, useUser } from "skybridge/web";
import { useToolInfo } from "../helpers.js"; // generated, type-safe from server schema

export default function Carousel() {
  const { output } = useToolInfo<"search-products">();
  const { theme, maxHeight, safeArea } = useLayout();
  const [mode, setMode] = useDisplayMode();
  const { userAgent } = useUser();

  return (
    <div
      className={theme === "dark" ? "dark" : ""}
      style={{ maxHeight, paddingBottom: safeArea.insets.bottom }}
    >
      <ProductGrid
        products={output.products}
        columns={mode === "fullscreen" ? 4 : 2}
        compact={userAgent.device.type === "mobile"}
      />
      {mode === "inline" && (
        <button onClick={() => setMode("fullscreen")}>See all</button>
      )}
    </div>
  );
}
The carousel caps its height at maxHeight and pads its bottom by safeArea.insets.bottom, so the last row clears the composer instead of hiding behind it.

Match the Theme

useLayout reports the host’s color scheme as theme, "light" or "dark". A view on its own palette looks pasted into the conversation; read theme and follow it.
views/carousel.tsx
import { useDisplayMode, useLayout, useUser } from "skybridge/web";
import { useToolInfo } from "../helpers.js"; // generated, type-safe from server schema

export default function Carousel() {
  const { output } = useToolInfo<"search-products">();
  const { theme, maxHeight, safeArea } = useLayout();
  const [mode, setMode] = useDisplayMode();
  const { userAgent } = useUser();

  return (
    <div
      className={theme === "dark" ? "dark" : ""}
      style={{ maxHeight, paddingBottom: safeArea.insets.bottom }}
    >
      <ProductGrid
        products={output.products}
        columns={mode === "fullscreen" ? 4 : 2}
        compact={userAgent.device.type === "mobile"}
      />
      {mode === "inline" && (
        <button onClick={() => setMode("fullscreen")}>See all</button>
      )}
    </div>
  );
}
The carousel adds a dark class when the host is dark, the convention Tailwind’s dark: variants key off, so its styles track the host.

Respond to the Display Mode

useDisplayMode returns the current mode and a setter. The three modes give the view different room and purpose:
  • inline is the default: a compact panel embedded in the conversation. The smallest surface, so it suits a single result or a short list.
  • fullscreen takes over the surface for richer, multi-step tasks.
  • pip(picture in picture) floats above the conversation and stays open, which fits live or changing content. On mobile it coerces to fullscreen.
Switching is user-triggered, and the host can decline the request.
views/carousel.tsx
import { useDisplayMode, useLayout, useUser } from "skybridge/web";
import { useToolInfo } from "../helpers.js"; // generated, type-safe from server schema

export default function Carousel() {
  const { output } = useToolInfo<"search-products">();
  const { theme, maxHeight, safeArea } = useLayout();
  const [mode, setMode] = useDisplayMode();
  const { userAgent } = useUser();

  return (
    <div
      className={theme === "dark" ? "dark" : ""}
      style={{ maxHeight, paddingBottom: safeArea.insets.bottom }}
    >
      <ProductGrid
        products={output.products}
        columns={mode === "fullscreen" ? 4 : 2}
        compact={userAgent.device.type === "mobile"}
      />
      {mode === "inline" && (
        <button onClick={() => setMode("fullscreen")}>See all</button>
      )}
    </div>
  );
}
The carousel shows two columns inline and four in fullscreen, with a “See all” button that requests fullscreen only while inline. Keep inline compact, and let the denser layout wait for the room fullscreen gives you.

Adapt to the User

useUser reports the locale and the userAgent: the device type, and whether it supports hover and touch.
views/carousel.tsx
import { useDisplayMode, useLayout, useUser } from "skybridge/web";
import { useToolInfo } from "../helpers.js"; // generated, type-safe from server schema

export default function Carousel() {
  const { output } = useToolInfo<"search-products">();
  const { theme, maxHeight, safeArea } = useLayout();
  const [mode, setMode] = useDisplayMode();
  const { userAgent } = useUser();

  return (
    <div
      className={theme === "dark" ? "dark" : ""}
      style={{ maxHeight, paddingBottom: safeArea.insets.bottom }}
    >
      <ProductGrid
        products={output.products}
        columns={mode === "fullscreen" ? 4 : 2}
        compact={userAgent.device.type === "mobile"}
      />
      {mode === "inline" && (
        <button onClick={() => setMode("fullscreen")}>See all</button>
      )}
    </div>
  );
}
The carousel renders a compact card on mobile. Reach for capabilities.hover before relying on hover affordances, and locale to translate copy.

Iterate

The DevTools environment inspector flips theme, display mode, locale, and device type live, so you can watch the view adapt on localhost, then confirm it in a real host through the tunnel.

Go Further

Create Views

Craft interactive UIs rendered in conversation

Handle Files

Move files in and out of your app

Configure CSP

Let your views reach the domains they need