Seamlessly Moving an UNO Card Across Multiple Tabs
Occasianly, I saw a demo demonstated by someone showing a card moving across different window seamlessly, but he didn’t give out the code. But I found it interesting, so I tried to realize it by myself and also want to share this little demo with you.
What the demo does
- Opens the same URL in multiple same-origin windows and place them on the screen seamlessly
- Click and move to grab and move the card across different window seamlessly
- The result feels like one card moving across tabs, but technically it is several synchronized replicas.
Use screen coordinates as the source of truth
The shared state stores the card in desktop space, not page space:
const CARD_WIDTH = 308;
const CARD_HEIGHT = 428;
function defaultState() {
const rect = getViewportRect();
return {
card: {
screenX: Math.round(rect.x + (rect.width - CARD_WIDTH) / 2),
screenY: Math.round(rect.y + (rect.height - CARD_HEIGHT) / 2),
width: CARD_WIDTH,
height: CARD_HEIGHT,
rotation: CARD_ROTATION,
},
drag: null,
updatedAt: Date.now(),
};
}
Once the card lives in screen space, every window can map it back into local coordinates.
Track the real viewport, not just screenX and screenY
window.screenX and window.screenY describe the outer browser window, not the page content area. Browser chrome matters here: tabs, toolbar, bookmarks bar, and window frame all shift the viewport.
This demo estimates the content origin like this:
function getViewportRect() {
const windowX = window.screenX ?? window.screenLeft ?? 0;
const windowY = window.screenY ?? window.screenTop ?? 0;
const horizontalChrome = Math.max(0, window.outerWidth - window.innerWidth);
const verticalChrome = Math.max(0, window.outerHeight - window.innerHeight);
const leftFrame = Math.round(horizontalChrome / 2);
const bottomFrame = Math.min(16, Math.round(verticalChrome * 0.16));
const topChrome = Math.max(0, verticalChrome - bottomFrame);
return {
x: Math.round(windowX + leftFrame),
y: Math.round(windowY + topChrome),
width: window.innerWidth,
height: window.innerHeight,
};
}
This is not perfect across every browser, but it is good enough for a convincing desktop demo.
Render the card only if it intersects the current window
Each window computes whether the shared card overlaps its own viewport:
function intersectsViewport(card, viewport) {
return (
card.screenX + card.width > viewport.x &&
card.screenX < viewport.x + viewport.width &&
card.screenY + card.height > viewport.y &&
card.screenY < viewport.y + viewport.height
);
}
function toLocalCard(card, viewport) {
return {
x: card.screenX - viewport.x,
y: card.screenY - viewport.y,
width: card.width,
height: card.height,
rotation: card.rotation,
};
}
If there is no intersection, the window renders nothing. If there is, it draws its local replica at the translated position.
Sync state with localStorage and BroadcastChannel
localStorage gives persistence and cross-window updates. BroadcastChannel makes synchronization immediate.
function persistState(nextState) {
state = sanitizeState(nextState);
localStorage.setItem(STATE_KEY, JSON.stringify(state));
channel?.postMessage({
type: "state",
source: tabId,
payload: state,
});
render();
}
This combination works well for same-origin demos and avoids extra infrastructure.
Convert drag events back into screen space
During drag, the pointer is local to the current window, so the code converts it back into screen coordinates before updating shared state:
function localPointerToScreen(clientX, clientY, drag) {
const viewport = getViewportRect();
return {
x: Math.round(viewport.x + clientX - drag.offsetX),
y: Math.round(viewport.y + clientY - drag.offsetY),
};
}
That single conversion is what makes the whole trick work.
Each window owns a hidden copy
There is no DOM node moving across browser windows. That is not possible. Instead:
- Every window contains the same card markup
- All windows listen to the same shared state
- Each one decides when to show its own copy
This is a common pattern for “seamless multi-window” demos: shared world state, local rendering.
Final notes
A few constraints remain:
- Drag events disappear when the cursor enters browser chrome or desktop gaps
- Viewport estimation is browser-dependent
- This demo works only across same-origin windows
Even with those limits, the pattern is useful. If you want a convincing multi-window illusion in the browser, screen-space state plus per-window replicas is the simplest foundation.
Comments