ScreenshotNeo

BlogEngineering

How to Focus a Tab After Capturing with the getDisplayMedia API

Use CaptureController.setFocusBehavior() to request focus for a captured tab, handle windows and monitors safely, and avoid common browser errors.

By the ScreenshotNeo team1 October 20267 min read

Direct answer: create a CaptureController, pass it to navigator.mediaDevices.getDisplayMedia(), then call controller.setFocusBehavior("focus-captured-surface") after the promise resolves. Check the returned video track’s displaySurface first: this behavior can request focus for a browser tab or window, but a monitor cannot be focused with this method.

The request is conditional. The browser still controls the picker, permission prompt, and final focus decision. Focus behavior must be set before the decision is finalized, and browser support is currently limited. See the MDN API reference and the W3C Screen Capture Working Draft.

What the API does

getDisplayMedia() asks the user to choose a browser tab, window, or monitor and returns a MediaStream. A CaptureController lets your application express a focus preference for that capture. The available preferences are:

Value Meaning Typical use
focus-captured-surface Request focus move to the selected tab or window. A presentation or recording app that should make the shared surface visible.
no-focus-change Ask the browser to leave the current focus unchanged. Your capture controls should remain active while sharing.
focus-capturing-application A preference defined in the current draft specification. Use only after checking support in your target browsers.

The user must make the selection. The API requires a secure context and transient user activation, and permission cannot be silently reused for a future capture. Capture constraints can guide presentation, but they cannot remove the user’s choice.

Minimal working example

Run this code from a button click on an HTTPS page (localhost is also treated as secure by browsers). It requests focus only when the user selects a browser tab.

<button id="share">Share a tab or window</button>
<video id="preview" autoplay muted playsinline></video>
<pre id="status"></pre>

<script>
const button = document.querySelector('#share');
const preview = document.querySelector('#preview');
const status = document.querySelector('#status');

button.addEventListener('click', async () => {
  let controller;

  try {
    controller = new CaptureController();
  } catch (error) {
    status.textContent = 'CaptureController is not supported in this browser.';
  }

  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: true,
      audio: false,
      ...(controller ? { controller } : {})
    });

    preview.srcObject = stream;
    const [track] = stream.getVideoTracks();
    const surface = track.getSettings().displaySurface;

    if (controller && (surface === 'browser' || surface === 'window')) {
      controller.setFocusBehavior('focus-captured-surface');
      status.textContent = `Capturing a ${surface}; focus requested.`;
    } else if (surface === 'monitor') {
      status.textContent = 'Capturing a monitor; this API cannot focus a monitor.';
    } else {
      status.textContent = `Capturing ${surface || 'an unknown surface'}.`;
    }

    track.addEventListener('ended', () => {
      status.textContent = 'The user stopped sharing.';
    });
  } catch (error) {
    status.textContent = `${error.name}: ${error.message}`;
  }
});
</script>

Set the preference before or immediately after capture

You may set a focus preference before calling getDisplayMedia(), or once immediately after the promise fulfills. The following pattern requests that the captured surface receive focus while the picker is being resolved:

const controller = new CaptureController();
controller.setFocusBehavior('focus-captured-surface');

const stream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  controller
});

Setting it after the browser has finalized its focus decision is too late. Calling it repeatedly is allowed only while that decision is still pending. Treat the method as a request to the user agent, not a guarantee that the operating system will switch windows.

Choose behavior from the selected surface

The video track reports the selected surface through track.getSettings().displaySurface. Typical values are browser for a tab, window for an application window, and monitor for an entire screen.

const [track] = stream.getVideoTracks();
const { displaySurface } = track.getSettings();

switch (displaySurface) {
  case 'browser':
    controller.setFocusBehavior('focus-captured-surface');
    break;
  case 'window':
    // Decide whether the shared window or your app should stay focused.
    controller.setFocusBehavior('no-focus-change');
    break;
  case 'monitor':
    // Do not request focus for a monitor.
    break;
  default:
    // Unknown values should be handled conservatively.
    break;
}

Do not assume the user selected a tab. A monitor is not focusable through this API, and attempting to apply the focus behavior to a monitor can raise InvalidStateError.

Keep the capture app focused instead

If your controls, notes, or recording status must remain visible, request no-focus-change:

const controller = new CaptureController();
const stream = await navigator.mediaDevices.getDisplayMedia({ controller });

const [track] = stream.getVideoTracks();
if (track.getSettings().displaySurface !== 'monitor') {
  controller.setFocusBehavior('no-focus-change');
}

The direction matters: focus-captured-surface asks to move focus to the shared tab or window; no-focus-change asks the browser to leave focus where it is.

Constraints and picker options

Constraints affect the returned video track, not the user’s permission decision. A practical request can include frame-rate or resolution preferences:

const controller = new CaptureController();
const stream = await navigator.mediaDevices.getDisplayMedia({
  controller,
  video: {
    width: { ideal: 1920 },
    height: { ideal: 1080 },
    frameRate: { ideal: 30, max: 60 }
  },
  audio: false
});

Use the options documented for your target browser. Presentation hints may influence the picker, but the user can still choose another surface. Validate the actual track settings after capture instead of relying on requested values.

Lifecycle and cleanup

Stop every track when the user finishes. This releases the capture indicator and prevents a stale stream from keeping resources open.

function stopCapture(stream) {
  for (const track of stream.getTracks()) {
    track.stop();
  }
  preview.srcObject = null;
}

stream.getVideoTracks()[0].addEventListener('ended', () => {
  preview.srcObject = null;
});

For recording or WebRTC, pass the same stream to MediaRecorder or an RTCPeerConnection. Focus behavior affects the user interface; it does not change the pixel data delivered by the track.

Browser support and security requirements

  • Serve the page from a secure context (HTTPS; localhost is generally permitted for development).
  • Start the call from a transient user gesture such as a click.
  • Expect a fresh permission prompt and user surface selection.
  • Feature-detect CaptureController and setFocusBehavior; MDN marks them limited availability and not Baseline.
  • Test the visible focus transition in every browser and version you support. The user agent may disable focus changes using its own logic.

Troubleshooting

Symptom or error Cause Fix
NotAllowedError No user activation, permission denied, or an insecure context. Call from a click or key handler, use HTTPS, and ask the user to approve a surface.
InvalidStateError from focus behavior The decision window has closed, the controller is not associated with the stream, or the selected surface is a monitor. Pass the controller in getDisplayMedia(), set the preference before or immediately after resolution, and skip monitors.
TypeError or unsupported method The browser does not implement CaptureController or the requested value. Feature-detect and provide a normal capture fallback.
The wrong surface receives focus The user selected a window or monitor, or the browser ignored the request. Inspect displaySurface, explain the behavior in your UI, and do not promise guaranteed focus.
Focus request appears to do nothing The browser’s focus policy, platform rules, or timing prevented the change. Set the preference earlier and test the exact browser/platform combination.
Capture starts but video is blank The stream was not attached to a video element or playback was blocked. Set video.srcObject = stream, use autoplay muted playsinline, and handle the play() promise.
Sharing stops unexpectedly The user clicked the browser’s stop-sharing control or the source closed. Listen for the track’s ended event and reset application state.

Performance and reliability

  • Focus changes do not add a second video encode or copy; they are a browser UI preference.
  • Resolution and frame rate determine capture bandwidth and encoding cost. Request only what your recorder or call needs.
  • Read actual settings after capture and adapt your layout to the selected dimensions.
  • Keep the user gesture close to getDisplayMedia(); asynchronous work before the call can consume transient activation.
  • Assume focus is best-effort. Your application must remain usable when the request is ignored.
  • Stop tracks on completion, navigation, and errors to avoid leaked capture sessions.

Or skip the browser setup

If you need an image or PDF of a URL rather than an interactive user-selected screen, ScreenshotNeo handles the capture on its API. It removes cookie banners, newsletter popups, and chat widgets before the shot; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; and its MCP server lets AI agents take screenshots.

See the ScreenshotNeo API documentation. cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

There is a free plan with 1,000 screenshots per month and no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can I focus a tab without asking the user?

No. getDisplayMedia() requires an explicit user selection and permission. The focus method only expresses a preference after that selection.

Can I focus an entire monitor?

No. The focus behavior applies to focusable tabs or windows. Skip the call when displaySurface is monitor.

Should I use focus-captured-surface or no-focus-change?

Use the first when the shared tab or window should become active. Use the second when your capture application should keep focus.

Is setFocusBehavior() supported everywhere?

No. MDN currently lists it as limited availability and not Baseline. Feature-detect it and test the user-visible behavior in your supported browsers.

Does focus behavior change the captured pixels?

No. It requests a focus change in the browser or operating system. The video track still contains the selected surface’s pixels.