# Layout
URL: /remote-components/docs/layout

A remote lays out like its content in a block. Width comes from your layout. Height comes from the content.

> For AI agents: a documentation index is available at [llms.txt](/llms.txt). Use `.md` for canonical markdown pages; `.mdx` is kept as a backwards-compatible alias on supported URL paths.

The element you style is the *slot*. `className` and `style` on `<RemoteComponent>` go to it. Think of it as a `<div style="display: flow-root">` around the page's content.

Inside the frame, the page's `<html>` element is the slot. The runtime owns its box: width, height, position, margin, padding, border and overflow. Everything inside it is yours, and your CSS wins there. The body has no default margin.

## Width and height

The host sends the exact width, fractions included. The remote measures its height at that width and reports it back. Drag the slider.

\[interactive preview omitted]

```
<div style={{ width }}>
  <RemoteComponent
    src="https://widgets.example/note.html"
    title="Quarterly report"
    text="Revenue grew in every region…"
  />
</div>
```

widgets/note.html

```html
<div style="font-weight: 500" id="title"></div>
<p class="muted" style="margin: 4px 0 0" id="text"></p>
<script>
  function render() {
    document.querySelector("#title").textContent = rc.props.title ?? "";
    document.querySelector("#text").textContent = rc.props.text ?? "";
  }
  render();
  rc.addEventListener("props-changed", render);
</script>
```

## Flex, grid and fit-content

The remote also reports its min-content and max-content widths. Shrink-to-fit layouts use them like they would for inline content. The tags take their natural width, then wrap when there is no room.

\[interactive preview omitted]

```
<div style={{ display: "flex", gap: 8 }}>
  <span>Labels</span>
  <RemoteComponent src={tagsUrl} tags={tags} style={{ flex: "0 1 auto", minWidth: 0 }} />
  <span>Edit</span>
</div>

<div style={{ display: "grid", gridTemplateColumns: "auto minmax(min-content, max-content) auto", gap: 8 }}>
  <span>Labels</span>
  <RemoteComponent src={tagsUrl} tags={tags} />
  <span>Edit</span>
</div>

<span>Labels</span>
<RemoteComponent src={tagsUrl} tags={tags} style={{ width: "fit-content", maxWidth: "100%" }} />
<span>Edit</span>
```

widgets/tags.html

```html
<style>
  .tag { padding: 2px 8px; border: 1px solid var(--color-border); border-radius: 999px; font-size: 0.875em; }
</style>
<div class="row" style="gap: 4px"></div>
<script>
  const row = document.querySelector(".row");
  function render() {
    row.replaceChildren(
      ...(rc.props.tags ?? []).map((tag) => {
        const span = document.createElement("span");
        span.className = "tag";
        span.textContent = tag;
        return span;
      }),
    );
  }
  render();
  rc.addEventListener("props-changed", render);
</script>
```

## Filling the slot

When the slot has a definite height, percentage heights in the remote resolve against it. That covers flex and grid stretch, `flex: 1` in a column, `aspect-ratio` and fixed heights. Otherwise `height: 100%` behaves as auto.

\[interactive preview omitted]

```
<div style={{ display: "flex", gap: 8, height }}>
  <nav>Sidebar</nav>
  <RemoteComponent src={panelUrl} title="Remote panel" style={{ flex: 1 }} />
</div>
```

widgets/panel.html

```html
<style>
  .panel {
    height: 100%;
    min-height: 48px;
    display: grid;
    place-items: center;
    border: 1px dashed var(--color-border-strong);
    border-radius: var(--radius-md);
    background: var(--color-surface-muted);
  }
</style>
<div class="panel"><span class="muted"></span></div>
<script>
  const render = () => (document.querySelector(".panel span").textContent = rc.props.title ?? "Panel");
  render();
  rc.addEventListener("props-changed", render);
</script>
```

## The slot is the viewport

Pages are often written for a browser window. They use `100vh` and center themselves in it. In a frame that grows with its content, that would loop: the content fills the viewport, the viewport grows, and so on.

So inside a remote, `vh` means the slot. With a height you set, `100vh` is that height. With an auto height, `height` and `min-height` in `vh` become auto, and the content decides. The page below sets `min-height: 100vh` on its body.

\[interactive preview omitted]

```
<RemoteComponent src="https://widgets.example/centered.html" style={{ height }} />
```

widgets/centered.html

```html
<!doctype html>
<html>
  <head>
    <!-- Written for a browser window: full height, content in the middle. -->
    <style>
      body { min-height: 100vh; display: grid; place-items: center; }
      .card { display: grid; gap: 4px; padding: 12px 16px; border: 1px solid var(--color-border); border-radius: var(--radius-md); text-align: center; }
      .muted { color: var(--color-text-muted); }
    </style>
  </head>
  <body>
    <div class="card">
      <strong>Payment received</strong>
      <span class="muted">$240.00 from Acme Inc.</span>
    </div>
  </body>
</html>
```

`vw` is the slot width. Container queries and `cq` units see your containers, even when the slot sits inside a wider one. The rewrite covers stylesheets from the page's origin, inline styles and CSS-in-JS. JavaScript that reads `innerHeight` still sees the frame.

## What does not cross

- Baselines. The slot has no text baseline, so baseline alignment uses its bottom edge.
- Overflow past a height you constrain is clipped by the frame.
- `position: fixed` is relative to the slot. A fixed bar sits at the bottom of the component. To draw outside it, use an [overlay](/remote-components/docs/overlays).
- LCD text anti-aliasing. Text in a transparent frame is grayscale.

Margins inside the page don't collapse through the slot, as with `flow-root`.