App instance from @modelcontextprotocol/ext-apps. If you haven’t, start with the SDK Quickstart. To match the host theme, let the host background show through, then apply the host style variables at runtime.
Let the host background show through
Every frame Claude places between your widget and the chat surface is already transparent, so transparency holds as long as your own document doesn’t paint over it. Leave the body background unpainted, declarecolor-scheme, and request a borderless frame.
Don’t paint a body background
Any opaque background on<html> or <body> hides the chat surface behind it. Explicitly set both to transparent:
Declare color-scheme in your document head
Browsers give iframe documents an opaque canvas backdrop, white in light mode and near-black in dark mode, when the iframe’s color-scheme differs from the embedding page. Declaring both schemes opts your document into whichever mode the host is in, so the browser drops the backdrop and makes the CSS light-dark() values in Claude’s tokens resolve correctly:
Request a borderless frame
SetprefersBorder: false in your UI resource’s _meta.ui object so the host doesn’t wrap your widget in its own bordered card. Claude web’s default is already borderless, but other hosts differ, so being explicit keeps your app portable. Register the resource with registerAppResource:
Apply the host style variables
Claude passes ahostContext object to your widget during the connect() handshake. The fields relevant to theming are:
The Style variables section of the design guidelines lists every variable and its light- and dark-mode value.
Read hostContext and listen for changes
The App class exposes the initial context via getHostContext() once connect() resolves, and delivers subsequent updates, such as the user toggling dark mode, through the hostcontextchanged event. Register the listener before you connect so you don’t miss an early update.
The SDK provides helpers that do the DOM work for you, and React hooks that wrap them:
applyDocumentTheme(theme)sets<html data-theme>and the rootcolor-scheme, so[data-theme="dark"]selectors andlight-dark()values resolve correctlyapplyHostStyleVariables(variables)writes every entry instyles.variablesonto:rootas a CSS custom propertyapplyHostFonts(fontCss)injects the host’s@font-facerules onceuseApp(options)creates and connects theAppinstance for you in ReactuseHostStyles(app, hostContext)applies the theme, variables, and fonts and re-applies onhostcontextchanged
<meta name="color-scheme"> tag in your document head even though applyDocumentTheme also sets color-scheme at runtime. The tag covers the first paint before your script runs and prevents an opaque-backdrop flash.
This example applies the theme, variables, and fonts on connect and again on every host context change:
Reference the variables in your CSS
Once the variables are on:root, reference them directly. Provide fallbacks so the widget is still readable when rendered outside a host:
Claude’s token values use CSS
light-dark(), so once applyDocumentTheme has set the root color-scheme, every --color-* variable resolves to the right variant without any [data-theme] selectors on your side.Allow the host font origin in your CSP
ForapplyHostFonts to load the @font-face files, your resource’s _meta.ui.csp allowlist must include https://assets.claude.ai in resourceDomains, as the registerAppResource snippet shows. resourceDomains also adds the listed origins to script-src and style-src, so keep it to origins you trust to serve executable code. Prefer bundling third-party fonts into your widget rather than allowlisting public CDNs.
Next steps
- Style variables: the full variable palette in the design guidelines
- Visual design: usage guidance for color, typography, and spacing in the design guidelines
- Supersede older widget instances: keep only the newest copy of your widget active in a conversation
- SDK API reference:
App,McpUiHostContext, andMcpUiResourceMeta