# Keyboard and focus
URL: /remote-components/docs/keyboard

Tab moves through your page and the remote as if they were one document.

> 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.

## Tab order

Click the Name field and press Tab. Focus goes into the remote, through its stops, and out to Pay. Shift+Tab goes back the same way. The radio group is one stop, and the arrow keys move inside it.

\[interactive preview omitted]

```
const ref = useRef<RemoteComponentHandle>(null);

<input placeholder="Name" />
<RemoteComponent ref={ref} src="https://widgets.example/shipping.html" onSave={save} />
<button>Pay</button>

// Move focus into the remote yourself, e.g. after an error:
ref.current?.focus("first", { focusVisible: true });
```

widgets/shipping.html

```html
<form class="row">
  <input aria-label="ZIP code" placeholder="ZIP code" name="zip" style="width: 100px" />
  <fieldset class="radios">
    <legend>Speed</legend>
    <label><input type="radio" name="speed" value="Standard" /> Standard</label>
    <label><input type="radio" name="speed" value="Express" checked /> Express</label>
    <label><input type="radio" name="speed" value="Overnight" /> Overnight</label>
  </fieldset>
  <button type="submit">Save</button>
</form>
<script>
  const form = document.querySelector("form");
  form.onsubmit = (event) => {
    event.preventDefault();
    void rc.props.onSave({ zip: form.zip.value, speed: form.speed.value });
  };
</script>
```

Browsers disagree at the frame edge. Firefox stops on the frame itself, and WebKit can refuse focus right after a frame appears. So the host and the runtime handle Tab at the edge themselves and move focus to the next stop in document order. Nested remotes work the same way.

The tab order follows positive `tabindex` first, then document order. Open shadow roots are walked in place; closed ones are skipped.

## Focus ring

`:focus-visible` is a guess the browser makes from the last input. Inside a frame it only sees input in the frame, so a Tab on your page would show no ring after an earlier click in the remote. The host tells the remote when focus came from the keyboard. The ring shows when it would inline.

Give both sides the same ring. The examples here define `--focus-ring` on the page and use it in the remote stylesheet.

## Focus from code

`ref.focus("first" | "last", { focusVisible })` focuses the remote's first or last stop. Leave `focusVisible` out to let the browser decide. A remote with no stops passes focus on to the next one.

## Keys

- Key events inside the remote stay there. Your page's shortcuts don't fire while the remote has focus.
- Escape is the exception. If the remote doesn't handle it, the host closes the topmost overlay.
- When something in the remote gets focus, the host scrolls your scroll containers so it is visible.

Screen readers announce the frame by its title. Pass a short `frameTitle` that names the component.