loading
Warning:
Chromium only, over HTTPS The Window Management API ships in Chromium-based browsers on desktop. The first GetScreenDetails prompts for the window-management permission and so must run inside a user gesture; the resolved details are cached for the page, so later calls do not re-prompt.
Note:
How this differs from Screen The Screen service describes the one screen the window happens to be on and has no coordinate space to compare screens in. This one hands back every screen with Left/Top in a shared multi-screen space, which is what makes placing a window on the right monitor possible at all. IsExtended needs no permission, so it is the right gate for showing a "move to second screen" affordance in the first place.

Support, extension and permission

IsSupported / IsExtended / QueryPermission

IsSupported reports whether getScreenDetails exists; IsExtended reports whether more than one screen is attached, without any permission - the same reading Screen.IsExtended gives, offered here so a component already injecting this service needs no second one; QueryPermission reads the window-management state without prompting, and answers in PermissionState like every other permission read in the package.

C#
@inject Bit.Butil.WindowManagement windowManagement

var supported = await windowManagement.IsSupported();
var extended  = await windowManagement.IsExtended();
PermissionState state = await windowManagement.QueryPermission();
Live sample
support output
Results will appear here when you interact with the samples.

Enumerate the screens

GetScreenDetails

Returns every attached screen and which one this window is on. Null when the API is missing or the prompt was dismissed. Before permission is granted a browser reports only the current screen, so the list length is itself a hint about the permission state.

C#
var details = await windowManagement.GetScreenDetails();
// Null when the API is missing or the prompt was dismissed - there is nothing to enumerate.
if (details is null) return;

foreach (var screen in details.Screens)
{
    // screen.Label, screen.Left, screen.Top, screen.AvailWidth, screen.IsPrimary, screen.IsCurrent
}
Live sample
screens output
Results will appear here when you interact with the samples.

Place a window

OpenOnScreen

Opens a window positioned on the chosen screen. It is still a popup, so it needs a user gesture and the popup blocker still applies - false means it was blocked, or that the screen index does not exist.

C#
await windowManagement.OpenOnScreen("https://bitplatform.dev", screenIndex: 1, fullSize: true);
Live sample
URL
open output
Results will appear here when you interact with the samples.

Fullscreen on another screen

RequestFullscreenOnScreen

Takes an element fullscreen on the chosen screen rather than on the one the window is currently on. Needs a user gesture, like any fullscreen request.

C#
<div @ref="_stage">...</div>

await windowManagement.RequestFullscreenOnScreen(_stage, screenIndex: 1);
Live sample
This box goes fullscreen on the selected screen.
fullscreen output
Results will appear here when you interact with the samples.

Watch the screens change

SubscribeChange / RemoveChange / RemoveAllChanges

Fires when a monitor is attached or removed, and when this window is dragged onto a different screen. The handler gets the whole snapshot, because either event can invalidate every index the caller is holding.

C#
await using var subscription = await windowManagement.SubscribeChange(details =>
{
    // details!.Screens, details.CurrentScreenIndex
});
Live sample
change output
Results will appear here when you interact with the samples.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes window.getScreenDetails. Returns default (false) during prerender/SSR instead of throwing.
IsExtended
ValueTask<bool> IsExtended()
True when more than one screen is attached. Readable without any permission. The same reading as Screen.IsExtended, which it calls through to.
QueryPermission
ValueTask<PermissionState> QueryPermission()
The window-management permission state, as PermissionState. A query only - the prompt happens on the first GetScreenDetails. Equivalent to Permissions.Query('window-management'), and routed through it.
GetScreenDetails
ValueTask<ScreenDetails?> GetScreenDetails()
Every attached screen and which one this window is on. Prompts on the first call, so run it from a user gesture.
OpenOnScreen
ValueTask<bool> OpenOnScreen(string url, int screenIndex, bool fullSize = false, string? features = null)
Opens a window positioned on the chosen screen. Needs a user gesture; the popup blocker still applies.
RequestFullscreenOnScreen
ValueTask<bool> RequestFullscreenOnScreen(ElementReference element, int screenIndex)
Takes an element fullscreen on the chosen screen. Needs a user gesture.
SubscribeChange
ValueTask<ButilSubscription> SubscribeChange(Action<ScreenDetails?> handler)
Runs the handler when a screen is attached or removed, or when the window moves to another screen.
RemoveChange
ValueTask RemoveChange(Guid id)
Detaches one listener by the id its subscription carries.
RemoveAllChanges
ValueTask RemoveAllChanges()
Detaches every screens-change listener registered through this instance.
DisposeAsync
ValueTask DisposeAsync()
Detaches every listener and releases the JS callback reference.
InvokeScreensChange
void InvokeScreensChange(Guid id, ScreenDetails? details)
JSInvokable interop plumbing for the screens/current-screen change events - not intended for app code.
ScreenDetails
class ScreenDetails { bool IsExtended; int CurrentScreenIndex; ScreenDetailInfo[] Screens; }
Every attached screen, and which one this window is on.
ScreenDetailInfo
class ScreenDetailInfo { string Label; int Left; int Top; int Width; int Height; int AvailLeft; int AvailTop; int AvailWidth; int AvailHeight; int ColorDepth; int PixelDepth; double DevicePixelRatio; bool IsPrimary; bool IsInternal; string? OrientationType; int OrientationAngle; bool IsCurrent; }
One screen's geometry in the multi-screen coordinate space, plus its identity.
An unhandled error has occurred. Reload 🗙