Managing several displays with the Window Management API

Get information about connected displays and position windows relative to those displays.

Window Management API

The Window Management API allows you to enumerate the displays connected to your machine and to place windows on specific screens.

Suggested use cases

Examples of sites that may use this API include:

  • Multi-window graphics editors à la Gimp can place various editing tools in accurately positioned windows.
  • Virtual trading desks can show market trends in multiple windows any of which can be viewed in fullscreen mode.
  • Slideshow apps can show speaker notes on the internal primary screen and the presentation on an external projector.

How to use the Window Management API

The problem

The time-tested approach to controlling windows, Window.open(), is unfortunately unaware of additional screens. While some aspects of this API seem a little archaic, such as its windowFeatures DOMString parameter, it has nevertheless served us well over the years. To specify a window's position, you can pass the coordinates as left and top (or screenX and screenY respectively) and pass the desired size as width and height (or innerWidth and innerHeight respectively). For example, to open a 400×300 window at 50 pixels from the left and 50 pixels from the top, this is the code that you could use:

const popup = window.open(
  'https://example.com/',
  'My Popup',
  'left=50,top=50,width=400,height=300',
);

You can get information about the current screen by looking at the window.screen property, which returns a Screen object. This is the output on my MacBook Pro 13″:

window.screen;
/* Output from my MacBook Pro 13″:
  availHeight: 969
  availLeft: 0
  availTop: 25
  availWidth: 1680
  colorDepth: 30
  height: 1050
  isExtended: true
  onchange: null
  orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
  pixelDepth: 30
  width: 1680
*/

Like most people working in tech, I have had to adapt myself to the new work reality and set up my personal home office. Mine looks like on the photo below (if you are interested, you can read the full details about my setup). The iPad next to my MacBook is connected to the laptop via Sidecar, so whenever I need to, I can quickly turn the iPad into a second screen.

School bench on two chairs. On top of the school bench are shoe boxes supporting a laptop and two iPads surrounding it.
A multi-screen setup.

If I want to take advantage of the bigger screen, I can put the popup from the code sample above on to the second screen. I do it like this:

popup.moveTo(2500, 50);

This is a rough guess, since there is no way to know the dimensions of the second screen. The info from window.screen only covers the built-in screen, but not the iPad screen. The reported width of the built-in screen was 1680 pixels, so moving to 2500 pixels might work to shift the window over to the iPad, since I happen to know that it is located on the right of my MacBook. How can I do this in the general case? Turns out, there is a better way than guessing. That way is the Window Management API.

Feature detection

To check if the Window Management API is supported, use:

if ('getScreenDetails' in window) {
  // The Window Management API is supported.
}

The window-management permission

Before I can use the Window Management API, I must ask the user for permission to do so. The window-management permission can be queried with the Permissions API like so:

let granted = false;
try {
  const { state } = await navigator.permissions.query({ name: 'window-management' });
  granted = state === 'granted';
} catch {
  // Nothing.
}

While browsers with the old and the new permission name are in use, be sure to use defensive code when requesting permission, as in the example below.

async function getWindowManagementPermissionState() {
  let state;
  // The new permission name.
  try {
    ({ state } = await navigator.permissions.query({
      name: "window-management",
    }));
  } catch (err) {
    if (err.name !== "TypeError") {
      return `${err.name}: ${err.message}`;
    }
    // The old permission name.
    try {
      ({ state } = await navigator.permissions.query({
        name: "window-placement",
      }));
    } catch (err) {
      if (err.name === "TypeError") {
        return "Window management not supported.";
      }
      return `${err.name}: ${err.message}`;
    }
  }
  return state;
}

document.querySelector("button").addEventListener("click", async () => {
  const state = await getWindowManagementPermissionState();
  document.querySelector("pre").textContent = state;
});

The browser can choose to show the permission prompt dynamically on the first attempt to use any of the methods of the new API. Read on to learn more.

The window.screen.isExtended property

To find out if more than one screen is connected to my device, I access the window.screen.isExtended property. It returns true or false. For my setup, it returns true.

window.screen.isExtended;
// Returns `true` or `false`.

The getScreenDetails() method

Now that I know that the current setup is multi-screen, I can obtain more information about the second screen using Window.getScreenDetails(). Calling this function will show a permission prompt that asks me whether the site may open and place windows on my screen. The function returns a promise that resolves with a ScreenDetailed object. On my MacBook Pro 13 with a connected iPad, this includes a screens field with two ScreenDetailed objects:

await window.getScreenDetails();
/* Output from my MacBook Pro 13″ with the iPad attached:
{
  currentScreen: ScreenDetailed {left: 0, top: 0, isPrimary: true, isInternal: true, devicePixelRatio: 2, …}
  oncurrentscreenchange: null
  onscreenschange: null
  screens: [{
    // The MacBook Pro
    availHeight: 969
    availLeft: 0
    availTop: 25
    availWidth: 1680
    colorDepth: 30
    devicePixelRatio: 2
    height: 1050
    isExtended: true
    isInternal: true
    isPrimary: true
    label: "Built-in Retina Display"
    left: 0
    onchange: null
    orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
    pixelDepth: 30
    top: 0
    width: 1680
  },
  {
    // The iPad
    availHeight: 999
    availLeft: 1680
    availTop: 25
    availWidth: 1366
    colorDepth: 24
    devicePixelRatio: 2
    height: 1024
    isExtended: true
    isInternal: false
    isPrimary: false
    label: "Sidecar Display (AirPlay)"
    left: 1680
    onchange: null
    orientation: ScreenOrientation {angle: 0, type: "landscape-primary", onchange: null}
    pixelDepth: 24
    top: 0
    width: 1366
  }]
}
*/

Information about the connected screens is available in the screens array. Note how the value of left for the iPad starts at 1680, which is exactly the width of the built-in display. This allows me to determine exactly how the screens are arranged logically (next to each other, on top of each other, etc.). There is also data now for each screen to show whether it is an isInternal one and whether it is an isPrimary one. Note that the built-in screen is not necessarily the primary screen.

The currentScreen field is a live object corresponding to the current window.screen. The object is updated on cross-screen window placements or device changes.

The screenschange event

The only thing missing now is a way to detect when my screen setup changes. A new event, screenschange, does exactly that: it fires whenever the screen constellation is modified. (Notice that "screens" is plural in the event name.) This means the event fires whenever a new screen or an existing screen is (physically or virtually in the case of Sidecar) plugged in or unplugged.

Note that you need to look up the new screen details asynchronously, the screenschange event itself does not provide this data. To look up the screen details, use the live object from a cached Screens interface.

const screenDetails = await window.getScreenDetails();
let cachedScreensLength = screenDetails.screens.length;
screenDetails.addEventListener('screenschange', (event) => {
  if (screenDetails.screens.length !== cachedScreensLength) {
    console.log(
      `The screen count changed from ${cachedScreensLength} to ${screenDetails.screens.length}`,
    );
    cachedScreensLength = screenDetails.screens.length;
  }
});

The currentscreenchange event

If I am only interested in changes to the current screen (that is, the value of the live object currentScreen), I can listen for the currentscreenchange event.

const screenDetails = await window.getScreenDetails();
screenDetails.addEventListener('currentscreenchange', async (event) => {
  const details = screenDetails.currentScreen;
  console.log('The current screen has changed.', event, details);
});

The change event

Finally, if I am only interested in changes to a concrete screen, I can listen to that screen's change event.

const firstScreen = (await window.getScreenDetails())[0];
firstScreen.addEventListener('change', async (event) => {
  console.log('The first screen has changed.', event, firstScreen);
});

New fullscreen options

Until now, you could request that elements be displayed in fullscreen mode via the aptly named requestFullScreen() method. The method takes an options parameter where you can pass FullscreenOptions. So far, its only property has been navigationUI. The Window Management API adds a new screen property that allows you to determine which screen to start the fullscreen view on. For example, if you want to make the primary screen fullscreen:

try {
  const primaryScreen = (await getScreenDetails()).screens.filter((screen) => screen.isPrimary)[0];
  await document.body.requestFullscreen({ screen: primaryScreen });
} catch (err) {
  console.error(err.name, err.message);
}

Polyfill

It is not possible to polyfill the Window Management API, but you can shim its shape so you can code exclusively against the new API:

if (!('getScreenDetails' in window)) {
  // Returning a one-element array with the current screen,
  // noting that there might be more.
  window.getScreenDetails = async () => [window.screen];
  // Set to `false`, noting that this might be a lie.
  window.screen.isExtended = false;
}

The other aspects of the API, that is, the various screen change events and the screen property of the FullscreenOptions, would simply never fire or silently be ignored respectively by non-supporting browsers.

Demo

If you are anything like me, you keep a close eye on the development of the various cryptocurrencies. (In reality I very much do not because I love this planet, but, for the sake of this article, just assume I did.) To keep track of the cryptocurrencies that I own, I have developed a web app that allows me to watch the markets in all life situations, such as from the comfort of my bed, where I have a decent single-screen setup.

Massive TV screen at the end of a bed with the author's legs partly visible. On the screen, a fake crypto currency trading desk.
Relaxing and watching the markets.

This being about crypto, the markets can get hectic at any time. Should this happen, I can quickly move over to my desk where I have a multi-screen setup. I can click on any currency's window and quickly see the full details in a fullscreen view on the opposite screen. Below is a recent photo of me taken during the last YCY bloodbath. It caught me completely off-guard and left me with my hands on my face.

The author with his hands on his panicking face staring at the fake crypto currency trading desk.
Panicky, witnessing the YCY bloodbath.

You can play with the demo embedded below, or see its source code on glitch.

Security and permissions

The Chrome team has designed and implemented the Window Management API using the core principles defined in Controlling Access to Powerful Web Platform Features, including user control, transparency, and ergonomics. The Window Management API exposes new information about the screens connected to a device, increasing the fingerprinting surface of users, especially those with multiple screens consistently connected to their devices. As one mitigation of this privacy concern, the exposed screen properties are limited to the minimum needed for common placement use cases. User permission is required for sites to get multi-screen information and place windows on other screens. While Chromium returns detailed screen labels, browsers are free to return less descriptive (or even empty labels).

User control

The user is in full control of the exposure of their setup. They can accept or decline the permission prompt, and revoke a previously granted permission via the site information feature in the browser.

Enterprise control

Chrome Enterprise users can control several aspects of the Window Management API as outlined in the relevant section of the Atomic Policy Groups settings.

Transparency

The fact whether the permission to use the Window Management API has been granted is exposed in the browser's site information and is also queryable via the Permissions API.

Permission persistence

The browser persists permission grants. The permission can be revoked via the browser's site information.

Feedback

The Chrome team wants to hear about your experiences with the Window Management API.

Tell us about the API design

Is there something about the API that does not work like you expected? Or are there missing methods or properties that you need to implement your idea? Have a question or comment on the security model?

  • File a spec issue on the corresponding GitHub repo, or add your thoughts to an existing issue.

Report a problem with the implementation

Did you find a bug with Chrome's implementation? Or is the implementation different from the spec?

  • File a bug at new.crbug.com. Be sure to include as much detail as you can, simple instructions for reproducing, and enter Blink>Screen>MultiScreen in the Components box. Glitch works great for sharing quick and easy repros.

Show support for the API

Are you planning to use the Window Management API? Your public support helps the Chrome team to prioritize features and shows other browser vendors how critical it is to support them.

Helpful links

Acknowledgements

The Window Management API spec was edited by Victor Costan, Joshua Bell, and Mike Wasserman. The API was implemented by Mike Wasserman and Adrienne Walker. This article was reviewed by Joe Medley, François Beaufort, and Kayce Basques. Thanks to Laura Torrent Puig for the photos.