A remote inherits from its slot like inline content does. Text styles and CSS variables cross the frame.
Switch the host theme below. The card has no colors or fonts of its own. Try the theme toggle in the header too: the color scheme follows.
<div className={theme}> {/* sets --foreground, --primary, font-family, … */}
<RemoteComponent src="https://widgets.example/theme-card.html" />
</div><!-- No colors or fonts of its own: everything comes from the host. -->
<div style="display: grid; gap: 8px">
<div style="font-weight: 600">Invoice #1042</div>
<div class="muted">Due in 14 days. Color scheme: <span id="scheme"></span>.</div>
<div class="row">
<button class="primary">Pay now</button>
<button>Download</button>
</div>
</div>
<script>
// CSS follows the host by itself. Script that reads computed styles re-reads them on theme-changed.
const show = () =>
(document.querySelector("#scheme").textContent =
getComputedStyle(document.documentElement).colorScheme || "normal");
show();
rc.addEventListener("theme-changed", show);
</script>In React, the same card reads the theme version from a hook:
import { useEffect, useState } from "react";
import { useThemeVersion } from "@assistant-ui/remote-components-react";
export default function ThemeCard() {
const version = useThemeVersion();
const [scheme, setScheme] = useState("");
useEffect(() => setScheme(getComputedStyle(document.documentElement).colorScheme), [version]);
return (
<div style={{ display: "grid", gap: 8 }}>
<div style={{ fontWeight: 600 }}>Invoice #1042</div>
<div className="muted">Due in 14 days. Color scheme: {scheme || "normal"}.</div>
<div className="row">
<button className="primary">Pay now</button>
<button>Download</button>
</div>
</div>
);
}Inherited properties
The host reads the slot's computed style and sends the inherited properties: color, color-scheme, the font-* family, line-height, letter-spacing, direction, accent-color and so on. Properties that don't inherit, like background or border, never cross. They belong to the slot.
The runtime applies them to the page's <html>, so everything inherits them. Your own CSS still wins, as it would inline. rem matches your root font size. The direction also sets <html dir>, so :dir(rtl) and Tailwind's rtl: variants match.
CSS variables
Every custom property defined at the slot crosses as it is. On top, the remote gets canonical tokens for anything you don't define: --color-text, --color-surface, --color-accent, --radius-md, --font-sans and more. They are read from shadcn/ui names when you have them (--foreground, --primary, …).
The example pages in these docs share one small stylesheet built on those tokens. It sits in each page as <style id="ui">:
*,
*::before,
*::after {
box-sizing: border-box;
}
button,
input,
select {
font: inherit;
color: inherit;
}
button {
height: 28px;
padding: 0 10px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
cursor: pointer;
white-space: nowrap;
}
button:hover {
background: var(--color-surface-muted);
}
button:focus-visible,
input:focus-visible {
outline: 2px solid var(--focus-ring, var(--color-accent));
outline-offset: 2px;
}
button.primary {
border-color: var(--color-accent);
background: var(--color-accent);
color: var(--color-accent-text);
}
button.primary:hover {
opacity: 0.9;
}
button:disabled {
opacity: 0.5;
cursor: default;
}
input:not([type="radio"]):not([type="checkbox"]) {
height: 28px;
padding: 0 8px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
min-width: 0;
}
.muted {
color: var(--color-text-muted);
}
.row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.mono {
font-family: var(--font-mono);
font-size: 0.875em;
}
.surface {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: 0 8px 24px rgb(0 0 0 / 0.12);
}
.menu {
padding: 4px;
display: grid;
}
.menu button {
border: 0;
background: transparent;
text-align: left;
border-radius: calc(var(--radius-md) - 4px);
}
.menu button:hover,
.menu button:focus-visible {
background: var(--color-surface-muted);
outline: none;
}
.radios {
display: flex;
align-items: center;
gap: 12px;
margin: 0;
padding: 0;
border: 0;
}
.radios legend {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.radios label {
display: inline-flex;
align-items: center;
gap: 6px;
}
.radios input {
margin: 0;
accent-color: var(--color-accent);
}
input[type="radio"]:focus-visible {
outline: 2px solid var(--focus-ring, var(--color-accent));
outline-offset: 2px;
}To limit what crosses, filter the variables on the provider:
<RemoteHostProvider
theme={{
vars: ["--brand-*", "--radius"], // only these custom properties
extraVars: { "--chart-1": "#2563eb" }, // added on top
}}
>Color scheme
color-scheme must match between the iframe element and the document inside it. Otherwise the browser paints an opaque background behind the remote. Tunnelling it keeps the frame transparent. So don't set color-scheme or a page background in the remote's own CSS.
Reading the theme from script
CSS needs nothing: variables and inherited properties update in place. Code that reads computed styles, like a canvas chart that reads --chart-1, re-reads them on theme-changed. It fires once at connect and after every change, when the runtime has applied the new values. In React, useThemeVersion() changes with each event. The card above uses it to show the color scheme.
function draw() {
const color = getComputedStyle(document.documentElement).getPropertyValue("--chart-1");
chart.update({ color });
}
draw();
rc.addEventListener("theme-changed", draw);Fonts
@font-face rules from your stylesheets are sent with absolute URLs. The remote loads them from its own origin, so font files need Access-Control-Allow-Origin. Opaque frames send Origin: null, which only * allows.
When it updates
On changes to the slot's className or style, to class, style, lang, dir or data-theme attributes anywhere in the document, to stylesheets, to prefers-color-scheme, and when the window or the slot resizes (media and container queries). At most once per frame, and only when something changed.
While a color or font transition runs on an element around the slot, the host reads the theme every frame. The remote fades along with your page and ends on the final values.