Device

@sushindustries/ui

A phone, a tablet or a laptop in CSS 3D, chosen by the stylesheet rather than by JavaScript, with a real screen in it.

7 min read
View as Markdown

Choosing one by hand

<Device kind="tablet">{...}</Device>
tsx

Every generated rule is written twice for this: once under its media query for .device:not([data-device]), once as .device[data-device="tablet"] with no query at all. The two selectors tie on specificity, the attribute forms come last, and so a choice beats a width.

That is what a settings panel writes, and it is how a showcase can put all three on one page.

The 3D, and which element carries what

Three flat elements laid out in space. Two properties do the work and putting either on the wrong element breaks the illusion in a way that is hard to name and easy to see.

perspective goes on the outer stage. One vanishing point for the whole machine. On the body instead, the body gets its own and the deck stops agreeing with it about where the viewer is standing.

transform-style: preserve-3d goes on the body, so the screen inside it stays in the same space rather than being flattened into a picture of itself.

rotateX is the tilt, and it is the only reason the perspective has anything to do. At zero this is an ordinary rectangle and the entire 3D stage is dead weight.

translateZ orders the screen and the back panel. Inside preserve-3d, paint order comes from 3D position rather than from the DOM.

The lens is longer on a laptop (1400px) than on a phone (1100px), which is the same instinct as a photographer's: the machine should look like it is being looked at, not like it is being photographed from six inches away.

The back panel must never be clickable

.device-back is inset: 0 over the whole body and comes after the screen in the DOM. The only thing keeping it behind is preserve-3d honouring its translateZ(-2px), and any ancestor that flattens the 3D context - a filter, an overflow, a browser quirk - puts it on top, where it silently swallows every click on the screen and looks completely fine doing it.

That happened. It is why the panel carries pointer-events: none as well.

The screen is a container

.device-screen declares container-type: inline-size, so everything inside sizes against the screen rather than against the window.

That is the difference between the machine being a frame and being a costume. A tablet chosen on a wide monitor is 40rem of screen inside a 1600px window; a @media query would give its contents a laptop's spacing, which is the exact thing choosing a tablet was meant to avoid.

.device-desktop {
	padding: clamp(var(--s-3), 3cqi, var(--s-4));
}
css

It does not animate open

It used to, on scroll. The lid rose as you reached it.

A frame around real controls should not make you wait to reach them, and this one is full of real controls - folders that open, windows that drag, a dock. The animation was a thing you sat through before the screen became readable, and it ran again every time somebody scrolled back up.

The dock belongs to the screen, not the desktop

The dock is a child of the screen, not of the desktop, so anything it opens is measured against the screen and clipped by the screen. Inside the desktop it would be measured against a scrolled box and cropped by it, which is a bug that looks like a rendering glitch and is really a containing block.

No window controls

Deliberately not three coloured dots. Those are one vendor's furniture, they mean close, minimise and zoom, and none of those three things can happen to this. Drawing controls that do nothing is worse than drawing none.

The windows inside the screen do have those actions, and they have real buttons for them. See DeskWindow.

Where this is used

WhereWhat
The home pagewith FolderShelf on the screen and a DockDockA launcher, what is open, and a corner. Search opens upward and results are the consumer's to compute.@sushindustries/ui · layout along the bottom
packages/atoms/devices.mdthe three machines, as a table
packages/atoms/src/devices.cssgenerated. The widths, as media queries
packages/ui/src/device-kinds.tsgenerated. The same widths, as values
useDeviceKinduseDeviceKindWhich machine the stylesheet is currently drawing, as a value. Null until mounted, on purpose - a default would be a claim the server cannot support.@sushindustries/ui · layoutfor code that has to name the machine rather than draw it
Used byuseDeviceKinduseDeviceKindWhich machine the stylesheet is currently drawing, as a value. Null until mounted, on purpose - a default would be a claim the server cannot support.@sushindustries/ui · layout