Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs-staging.skybridge.tech/llms.txt

Use this file to discover all available pages before exploring further.

How to make your view adapt to the host’s theme, locale, display mode, and device.

Available Hooks

HookWhat it provides
useLayoutTheme, safe areas, max height
useUserLocale, user agent, device capabilities
useDisplayModeCurrent display mode, mode switching
useRequestModalOpen view in modal
useOpenExternalOpen external URLs
MCP Apps Runtime: useRequestModal is polyfilled in MCP Apps. Modals render inside the view iframe rather than being portaled to the host. See useRequestModal API for details.

Theme Adaptation with useLayout

Match the host’s light/dark theme:
import { useLayout } from "skybridge/web";

function ThemedView() {
  const { theme } = useLayout();
  const isDark = theme === "dark";

  return (
    <div style={{
      backgroundColor: isDark ? "#1a1a1a" : "#ffffff",
      color: isDark ? "#ffffff" : "#000000",
      padding: "16px",
    }}>
      <h1>Hello</h1>
    </div>
  );
}

With CSS Variables

function ThemedView() {
  const { theme } = useLayout();

  return (
    <div
      className="view"
      data-theme={theme}
      style={{
        "--bg": theme === "dark" ? "#1a1a1a" : "#ffffff",
        "--text": theme === "dark" ? "#ffffff" : "#000000",
      } as React.CSSProperties}
    >
      {/* Content */}
    </div>
  );
}
.view {
  background: var(--bg);
  color: var(--text);
}

Safe Areas

Handle device notches and navigation bars:
const { safeArea } = useLayout();

<div style={{
  paddingTop: safeArea.top,
  paddingBottom: safeArea.bottom,
  paddingLeft: safeArea.left,
  paddingRight: safeArea.right,
}}>
  {/* Content */}
</div>

Max Height

Respect the container’s max height:
const { maxHeight } = useLayout();

<div style={{
  maxHeight: maxHeight ?? "100vh",
  overflow: "auto",
}}>
  {/* Scrollable content */}
</div>

User Info with useUser

Access locale and device capabilities:
Locale provided by OpenAIThe locale value comes from OpenAI’s host environment, not directly from the user’s browser. It may not always match the user’s actual browser locale settings.
import { useUser } from "skybridge/web";

function LocalizedView() {
  const { locale, userAgent } = useUser();

  // Format currency based on locale
  const formatPrice = (amount: number) => {
    return new Intl.NumberFormat(locale, {
      style: "currency",
      currency: "USD",
    }).format(amount);
  };

  // Check device capabilities
  const isMobile = userAgent.device.type === "mobile";
  const hasHover = userAgent.capabilities.hover;

  return (
    <div>
      <p>Price: {formatPrice(99.99)}</p>
      {isMobile ? (
        <button className="large-tap-target">Buy Now</button>
      ) : (
        <button className={hasHover ? "hoverable" : ""}>Buy Now</button>
      )}
    </div>
  );
}

Available User Agent Properties

const { userAgent } = useUser();

userAgent.device.type;        // "mobile" | "tablet" | "desktop"
userAgent.capabilities.hover; // true if device has hover capability
userAgent.capabilities.touch; // true if device has touch capability

Display Mode with useDisplayMode

Views can render in different display modes:
ModeDescription
pipPicture-in-picture (small floating window)
inlineInline with the conversation
fullscreenFull screen takeover
modalModal overlay
import { useDisplayMode } from "skybridge/web";

function AdaptiveView() {
  const { displayMode, setDisplayMode } = useDisplayMode();

  // Render differently based on mode
  if (displayMode === "pip") {
    return <CompactView />;
  }

  return (
    <div>
      <FullView />
      <button onClick={() => setDisplayMode("fullscreen")}>
        Go Fullscreen
      </button>
    </div>
  );
}

Requesting Mode Change

const { setDisplayMode } = useDisplayMode();

// Go fullscreen for immersive content
<button onClick={() => setDisplayMode("fullscreen")}>
  View Full Map
</button>

// Back to inline
<button onClick={() => setDisplayMode("inline")}>
  Close Map
</button>
Modal Display Mode is not accessible via useDisplayMode, it can only be opened via useRequestModal.
import { useRequestModal } from "skybridge/web";

function ProductCard({ product }) {
  const { open } = useRequestModal();

  const showDetails = () => {
    open({
      title: product.name,
      params: { productId: product.id },
    });
  };

  return (
    <div onClick={showDetails}>
      <img src={product.image} alt={product.name} />
      <p>{product.name}</p>
    </div>
  );
}

// In your modal component, access params via useToolInfo or similar
MCP Apps Runtime: useRequestModal is polyfilled in MCP Apps. Modals render inside the view iframe rather than being portaled to the host. See useRequestModal API for details.
Open URLs outside the view:
import { useOpenExternal } from "skybridge/web";

function LinkView() {
  const openExternal = useOpenExternal();

  const openDocs = () => {
    openExternal("https://docs.example.com");
  };

  const openApp = () => {
    // Open mobile app if available, do not include ChatGPT redirect URL
    openExternal("myapp://deep-link", { redirectUrl: false });
  };

  return (
    <div>
      <button onClick={openDocs}>View Documentation</button>
      <button onClick={openApp}>Open in App</button>
    </div>
  );
}
External links open in a new tab/window. Some URLs may be blocked by the iframe’s CSP policy.

Responsive Design Pattern

Combine hooks for a fully adaptive view:
function ResponsiveView() {
  const { theme, safeArea, maxHeight } = useLayout();
  const { locale, userAgent } = useUser();
  const { displayMode, setDisplayMode } = useDisplayMode();

  const isMobile = userAgent.device.type === "mobile";
  const isDark = theme === "dark";

  return (
    <div
      style={{
        backgroundColor: isDark ? "#1a1a1a" : "#ffffff",
        color: isDark ? "#ffffff" : "#000000",
        paddingTop: safeArea.top,
        paddingBottom: safeArea.bottom,
        maxHeight: maxHeight ?? "100vh",
        overflow: "auto",
      }}
    >
      {displayMode === "pip" ? (
        <MiniView onExpand={() => setDisplayMode("inline")} />
      ) : (
        <FullView
          isMobile={isMobile}
          locale={locale}
          onMinimize={() => setDisplayMode("pip")}
        />
      )}
    </div>
  );
}