Back to the Lab

NODE · node_browser18 Oct 2026In main · unreleased

CSS cannot reach a native window

NODE’s tabs are native Electron views, so Mosaic could not be animated in the stylesheet. I prototyped two ways and compared them.

Noticed

Mosaic is the moment NODE shows that several pages are live at once. Clicking it swapped the full page for the grid in a single frame, so you lost track of the page you were reading, and opening or closing a tab made every tile jump to its new cell. The usual fix — transform the elements — was not available: a tab in NODE is an Electron View, not an element, and nothing in styles.css can touch it.

Made

The research was macOS Mission Control, where windows shrink and spread and appear instantly with Reduce Motion on, and Vivaldi’s tab tiling. Then the finding that decided it: Electron 44, the version NODE runs, animates a view’s bounds natively through setBounds(bounds, { animate: { duration, easing } }). applyLayout() in main.js takes an optional value of “enter”, “exit” or “move”, and only the Mosaic toggle and opening or closing a tab in Mosaic pass it. Going in, the other tabs are already in their tiles underneath and the tab you were reading shrinks into its own, so your eye follows the page you care about; going back, that tile grows over the others and they are hidden only once they are covered, so nothing blinks out. Entering takes 320ms and leaving 200ms, because arriving at a new view needs a moment to read and leaving should get out of the way. This is the one place NODE animates size rather than transform, because a native view can only be moved and resized.

What the stacking turned on

win.contentView.children holds only the tab windows. NODE’s own interface is not in that list — it is the window’s base layer, under every tab — and index 0 is the bottom. In Focus only the active tab is attached, so it sits at index 0 and new tiles are inserted at its index, which puts each one directly under it. Calling addChildView on a view that is already attached raises it to the top without detaching it, which is how the active tile goes on top before it grows.

Live resize
Snapshot

Recorded from the approval prototype, not the running app. The prototype runs the same CSS on copies of NODE’s own controls; app recordings replace these when they are made. The two clips are the two approaches prototyped side by side.

Tokens it uses

MOTION_SLOW
320 in main.js, matching --motion-slow — Focus to Mosaic
MOTION_BASE
200 in main.js, matching --motion-base — Mosaic to Focus, and tiles reflowing
ease-out
Electron’s stand-in for --ease-enter
ease-in-out
Electron’s stand-in for --ease-move

Rules it follows

Enter and exit are not mirror images. Exits are faster

Going in takes 320ms, coming back 200ms. Arriving at a new view needs a moment to read; leaving should get out of the way.

Animate transform and opacity only

The fourth agreed exception is tab window bounds in Mosaic, and this is the only place NODE animates size. A native view cannot be transformed — it can only be moved and resized — so the tiles are resized for real on every frame.

Animate on user action only

Only the Mosaic toggle and opening or closing a tab while in Mosaic pass an animate value to applyLayout(). Every other layout change, including the ones a page causes by navigating, is instant.

Nothing bounces or overshoots. No spring curves

Electron offers four named curves and none of them spring. ease-out and ease-in-out are the closest to the tokens, which is as near as a native view gets.

Reduced motion removes the movement and keeps the fade

There is no fade to keep here, so Mosaic simply changes instantly, as it did before. The main process reads systemPreferences.getAnimationSettings().prefersReducedMotion at every change rather than once at startup.

Still open

Tested on Linux only so far — Electron 44 under a virtual display, recorded at 30 fps. All three moments animate: going in, the active page visibly shrinks into the bottom-right cell; going out, it grows while the other tiles stay until they are covered. The Mac pass, with heavy pages and video playing, has not been done. Electron also offers only four named curves, so ease-out and ease-in-out stand in for the custom tokens; they are close in feel but not the same curves.

Open the commit All of the Lab