Device Stage 3D
A pure-CSS 3D stage for phones, tablets and laptops with five arrangements: pointer tilt, scroll orbit, Y-axis flip, a fanned lineup of devices and a scroll-driven macro pull-back; screens are live DOM.
This is a WebberUI Pro component
The install command below needs a licence to run; without one it returns 401.
The classic product-page shot: put your app inside a phone or a laptop and let it move on the page. Every phone, tablet and laptop frame here is drawn in CSS (rounded corners, bezels, the pill-shaped island, side buttons, a keyboard deck) with no images, and the screen holds live DOM — text is selectable, buttons are clickable and screen readers can read it. Five arrangements are available: pointer tilt, scroll orbit, Y-axis flip (the back shows a second screen or a drawn back shell), a fanned lineup of three devices, and a scroll-driven pull-back from a macro close-up to the full view.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add "https://webberui.com/r/device-stage-3d.en.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<DeviceStage3D/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add "https://webberui.com/r/device-stage-3d.en.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/device-stage-3d.
Usage
import { DeviceStage3D } from "@/components/ui/device-stage-3d";
<DeviceStage3D
device="phone"
mode="flip"
screens={[<HomeScreen key="home" />, <SettingsScreen key="settings" />]}
caption={<p>點擊手機看設定頁</p>}
maxTilt={8}
/>;The caption above reads 點擊手機看設定頁 ("click the phone to see the settings screen").
orbit and macro are scroll-driven. When the stage sits inside a scrollable element, pass that element's ref to container:
const scrollerRef = React.useRef<HTMLDivElement>(null);
<div ref={scrollerRef} className="h-[480px] overflow-y-auto">
<div className="h-[320px]" />
<DeviceStage3D mode="macro" container={scrollerRef} screens={[<HomeScreen key="home" />]} />
<div className="h-[320px]" />
</div>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
screens | React.ReactNode[] | — | Screen content (1–3); flip uses the 2nd for the back, lineup fills three devices in order and repeats when short |
device | "phone" | "tablet" | "laptop" | "phone" | Device shape |
mode | "tilt" | "orbit" | "flip" | "lineup" | "macro" | "tilt" | Arrangement |
caption | React.ReactNode | — | Side caption; in flip mode it slides in again 120ms after every flip |
flipped | boolean | — | Controlled flipped state |
defaultFlipped | boolean | false | Initial flipped state in uncontrolled mode |
onFlippedChange | (flipped: boolean) => void | — | Called when the flipped state changes |
clickToFlip | boolean | true | In flip mode, clicking the device flips it, and a keyboard flip button is provided |
perspective | number | 1400 | Stage perspective distance (px) |
maxTilt | number | 8 | Maximum tilt angle in tilt mode (degrees) |
depthBlur | number | 3 | Starting blur (px) for out-of-focus layers in macro mode; 2–4 works best |
flipStiffness | number | 120 | Flip spring stiffness (damping is fixed at 20) |
gyro | boolean | false | Drive tilt from the gyroscope on touch devices |
container | React.RefObject<HTMLElement | null> | — | Scroll container that drives orbit/macro progress; defaults to the window |
screenClassName | string | — | Passed to every screen area |
className | string | — | Passed to the outermost container |
The DeviceStage3DDevice, DeviceStage3DMode and DeviceStage3DProps types are also exported by name.
CSS variables
| Field | Type | Default | Description |
|---|---|---|---|
--wb-ds3d-frame | color | #171717 (dark mode #262626) | Frame and side-button color |
--wb-ds3d-highlight | color | rgba(255,255,255,0.14) | 1px inner highlight on the frame |
--wb-ds3d-back | color | #e5e5e5 (dark mode #525252) | Starting color of the back-shell gradient |
--wb-ds3d-deck | color | #d4d4d4 (dark mode #404040) | Laptop keyboard deck color |
--wb-ds3d-glow | color | rgba(115,115,115,0.16) | Soft glow behind the stage |
How it works
- tilt: the pointer position relative to the stage center becomes −1 to 1 and maps to rotateY ±maxTilt and rotateX ∓maxTilt, smoothed by a stiffness 150 / damping 18 spring; leaving the stage returns it to center. Touch devices (
pointer: coarse) do not follow by default; withgyroon it readsdeviceorientationinstead, and iOS asks for permission after the first touch - orbit: scroll progress 0 to 1 maps to rotateY −30° to 30°, and translateZ peaks at 40px in the middle
- flip: rotateY 0 to 180°,
screens[0]on the front andscreens[1]on the back; with a single screen the back is drawn as a shell (camera module and matte finish). Clicking buttons or links inside the screen does not flip - lineup: three devices fan out — the outer two turn inward 18°, shift out 60%, step back 80px and shrink to 0.9; when the stage enters the viewport they spring up from below 0.12s apart
- macro: while scrolling until the stage is centered in the viewport, the group pulls back from 1.6× to 1× and its 12% offset returns to zero; the background decoration and side devices (when there is more than one screen) go from
depthBlurto sharp - laptop lid: the screen panel opens around its bottom edge from −90°; normally it springs open when entering the viewport, and in orbit/macro it is bound to the first 30% of scroll
Accessibility
- The outer element is
role="group"witharia-roledescription="device showcase"; the frame, side buttons, cameras and keyboard arearia-hiddendecoration, and screen content stays readable and focusable - Flip mode provides a keyboard flip button (
aria-pressedreflects whether the back is shown) that stays hidden until it receives Tab focus and responds to Enter/Space; the face turned away from the user isinert, so it cannot be tabbed into - Side devices in macro are decoration only and are set to
aria-hiddenandinert - When the user's system has reduce motion turned on, every mode stays facing front and ignores the pointer and scroll, lineup appears in place, the laptop lid is already open, and flip becomes an in-place cross-fade between the two faces
Cassette Player
A skeuomorphic cassette audio player: reels spin with playback time, tape spools grow and shrink with progress, plus an animated rewind, a draggable scrubber and caption tracks.
Cover Wall
A React grid wall of covers that fills in cell by cell from the centre outward, with a rolling counter and copy overlaid in the middle; hovering pushes a cell forward while neighbours recede.