react-3d-mockups
v0.1.1
Published
Interactive, GPU-accelerated 3D device mockups for React. Drop any content onto the screen of a 3D phone and it stays fully live.
Maintainers
Readme
react-3d-mockups
Live React components on procedural 3D devices - no GLB files. Docs, live demos and a gallery of all 51 models →
GPU-accelerated 3D device mockups for React. Put any content on the screen of a 3D device - real DOM, projected onto WebGL glass, so it stays live: text is vector crisp at any angle, videos play, iframes load, React state and effects keep running. Mockups are decorative: you rotate and zoom them, and the hardware masks the screen pixel for pixel (why).
In active development. The API may change over the next few weeks. Breaking changes are listed in the changelog with what to change; pin an exact version (
npm install --save-exact react-3d-mockups) if you need it to stay put.
- Thirty-three devices - the Galaxy S26 line (S26, S26 Ultra), two foldable
generations (Z Fold 7, the wide Z Fold 8, Z Fold 8 Ultra, Z Flip 7, Z Flip 8) and
Apple's foldable iPhone Duo, the full iPhone 17 family (17, 17 Air, 17 Pro, 17 Pro
Max) and the iPhone 18 Pro / Pro Max, MacBook Air 13"/15", MacBook Pro 14"/16" (M5)
and the MacBook Neo 13", iPad Pro 13"/11" (M5), iPad Air 13"/11" (M4), iPad (A16),
Galaxy Tab S11 / S11 Ultra, the Apple Watch Series 11, Series 12 and Ultra 4 and the
Galaxy Watch 8, Watch 9 and Watch Ultra 2 on
full wristbands, and a Studio Display-style 27" monitor, all procedurally generated
at runtime. No GLB files and nothing to host - importing one mockup costs about
11–56 KB gzipped (the whole library: 126 KB), peers excluded. The phones, foldables,
tablets, watches and laptops use a small CSG engine (
three-bvh-csg, a regular dependency) to machine their ports and speaker/mic holes into the chassis as real cavities; every other mockup leaves it out. - True-to-device screens - each virtual display matches the real device's logical resolution in portrait and landscape (table below), so your layouts and breakpoints behave exactly like on the hardware.
- Real GPU rendering, only when needed - three.js + react-three-fiber,
physically-based materials, studio lighting, soft shadows, clamped DPR. A mockup draws
a frame only when something moves (a drag,
autoRotate,float, a prop change), and not at all while it is scrolled off screen or its tab is hidden. - Any content on screen - pass React components, an
<iframe>or a<video>as children. State, effects and media playback keep running, and every surface is masked per-pixel by the hardware in front of it. - Composable - use the one-liners
<GalaxyMockup>/<IPhoneMockup>/<IPhoneDuoMockup>/<LaptopMockup>/<IPadMockup>/<GalaxyTabMockup>/<AppleWatchMockup>/<GalaxyWatchMockup>/<StudioDisplayMockup>, or drop<Galaxy>/<IPhone>/<Laptop>/<IPad>/<GalaxyTab>/<AppleWatch>/<GalaxyWatch>/<StudioDisplay>into your own react-three-fiber scene.
Install
npm install react-3d-mockups three @react-three/fiber @react-three/dreiReact 19 (react-three-fiber 9 and drei 10 both require it), three 0.179 to 0.186,
@react-three/fiber 9 and @react-three/drei 10. Those are
peer dependencies rather than bundled ones, because each has to exist exactly once in
an app - two copies of three mean two different THREE.Mesh classes, so instanceof
checks and r3f's element catalogue stop matching.
npm 7+ and pnpm 8+ install peers automatically, so npm install react-3d-mockups alone
already pulls all three in. Listing them explicitly still records them in your
package.json, which is what you want if you import from three yourself. Yarn does not
auto-install peers, so there the full command is required.
Quick start
'use client'
import { GalaxyMockup } from 'react-3d-mockups'
export function Hero() {
return (
<div style={{ height: 560 }}>
<GalaxyMockup autoRotate float>
<YourApp />
</GalaxyMockup>
</div>
)
}Drag anywhere - body, background, or the screen itself - to orbit; with the canvas
focused, the arrow keys turn it and Home resets it. In Next.js, load it client-side only
(dynamic(() => import('./mockup'), { ssr: false })) and give the wrapper a fixed height
so the page does not shift when the canvas mounts.
Regions & slots
Bare children always fill a mockup's primary region (a phone's screen, a book's
cover). Objects with more printable surfaces expose each one as a compound slot -
type AFrameSignMockup. and your editor lists exactly the regions the object has:
<AFrameSignMockup autoRotate>
<AFrameSignMockup.Front>
<MenuBoard />
</AFrameSignMockup.Front>
<AFrameSignMockup.Back surfaceBackground="#20241f" resolution={640}>
<HoursBoard />
</AFrameSignMockup.Back>
</AFrameSignMockup>A slot takes the same three surface settings a mockup does - surfaceBackground,
resolution, surfaceStyle - and overrides the mockup's for that one region. One
vocabulary, whichever element you hang it off. Every surface has a name, so content
lands where the name says no matter what order you write the slots in:
<BrochureMockup>
<BrochureMockup.FrontLeft><Cover /></BrochureMockup.FrontLeft>
<BrochureMockup.FrontCenter><Middle /></BrochureMockup.FrontCenter>
<BrochureMockup.BackLeft><Map /></BrochureMockup.BackLeft>
</BrochureMockup>The same slots exist on the raw scene components (<AFrameSign.Front> inside your own
r3f scene). Slots must be direct children of their mockup (fragments are fine); region
names come from each object's spec in the core.
Components
<GalaxyMockup> / <IPhoneMockup> / <LaptopMockup> / <IPadMockup> / <GalaxyTabMockup> / <AppleWatchMockup> / <GalaxyWatchMockup> / <StudioDisplayMockup> - all-in-one
Every device appearance prop, plus float (idle floating animation) and the staging
props from <MockupCanvas>: controls, autoRotate, zoom,
fullscreen, shadows, background, camera, frameloop, label,
screenAccessibility, className, style. The canvas props marked canvas only
below tune the renderer rather than the picture and stay on <MockupCanvas>. Transforms are first-class: position, rotation and scale flow
straight through to the device group (<IPhoneMockup rotation={[0, 0.25, 0]}>).
<MockupCanvas> - the stage
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| controls | boolean | true | Drag-to-orbit controls |
| freeRotation | boolean | false | Allow full 360° vertical rotation (straight over the top); off = classic clamped orbit. Canvas only |
| autoRotate | boolean \| number | false | Slowly orbit the camera. true is one revolution a minute; a number multiplies that (autoRotate={2} twice as fast) |
| zoom | boolean | false | Scroll/pinch zoom (off so pages don't lose scroll) |
| shadows | boolean | true | Soft contact shadow |
| shadowY | number | -2.05 | Y of the shadow plane (grounds the device). Canvas only - a mockup derives it from the object's framing |
| background | string | - | CSS background of the canvas |
| camera | r3f camera | [0, 0.5, 7.4], fov 40 | Camera override. Live: a new position/fov moves the camera there |
| dpr | number \| [min, max] | [1, 2] | Device-pixel-ratio clamp. Canvas only |
| frameloop | 'demand' \| 'always' \| 'never' | 'demand' | When to draw. 'demand' draws only when something changes; use 'always' for your own useFrame animation in a composed scene |
| pauseWhenOffscreen | boolean | true | Stop drawing while the canvas is off screen or its tab is hidden. Canvas only |
| time | number | - | Seconds on your own clock (a video's frame / fps): autoRotate and float follow it, so the same time draws the same picture |
| delayCapture | (reason) => () => void | - | Hold a video render or screenshot until the frame is complete (renderer started, redraw done, screens placed). Wire it to Remotion's delayRender/continueRender; while set, the canvas never pauses |
| gl | r3f gl | { antialias: true, alpha: true, powerPreference: 'default' } | Renderer settings, merged over the defaults. Keep alpha on - screens show through transparent pixels. Canvas only |
| onCreated | (state) => void | - | r3f's onCreated, e.g. to read gl.info. Canvas only |
| label | string | per model | Accessible name; the canvas is exposed as role="img" ("3D mockup of an iPhone") |
| screenAccessibility | 'hidden' \| 'visible' | 'hidden' | Screens are decorative, so by default they are aria-hidden and inert; 'visible' exposes their content |
<Galaxy> - the device
Render inside any r3f <Canvas>. Accepts all group props (position, rotation, scale…).
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| children | ReactNode | - | Screen content |
| color | string | '#101216' | Back panel, and the whole finish - the frame, buttons and (on the Ultra) the camera rings follow from it; the S26's rings are dark chrome on every finish, as on the hardware. A retail colorway id from GALAXY_COLORWAYS ('icyblue'…) brings that model's measured metal; any other CSS color gets metal derived from it |
| surfaceBackground | string | '#000000' | CSS background behind your content |
| variant | 's26' \| 's26ultra' | 's26' | Which Galaxy S26-family device (true relative sizes + per-model cameras) |
| orientation | 'portrait' \| 'landscape' | 'portrait' | Landscape lays the device sideways and swaps the virtual display |
| resolution | number | per variant | Virtual display width in CSS px (see resolution table) |
| surfaceStyle | CSSProperties | - | Extra styles for the screen wrapper |
<IPhone> - iPhone 17 family and iPhone 18 Pro
Same API as <Galaxy>, except: variant is '17' | 'air' | 'pro' | 'promax' | '18pro' |
'18promax', and resolution defaults to the variant's logical point grid (see resolution
table). Camera architecture follows the real devices: two-lens pill (17), ultra-thin
single-lens bar (Air), full-width triple-lens plateau with flash + LiDAR (the Pros). The
18 Pros are the 17 Pros' chassis under the generation's narrower Dynamic Island and
colour-matched back.
<IPhoneDuo> - iPhone Duo
Apple's foldable, on the same spec shape and pose vocabulary as <Fold>: openAngle
(true open onto the landscape 7.6" inner display, false shut onto the 5.4" cover,
or degrees for any Flex angle between), orientation, statusBar (iOS's), color (an
IPHONE_DUO_COLORWAYS id - 'nightsky', 'starwhite' - or any CSS color). The inner
display has no camera hole - the FaceTime camera sits under the glass - so nothing
interrupts your layout there.
<Laptop> - MacBook Air, MacBook Pro and MacBook Neo
Same screen API (surfaceBackground, resolution, surfaceStyle), plus
openAngle (lid angle, default 110), and resolution
defaulting to the variant's scaled desktop (Air 1280×832, Pro 14 1512×982, Neo 1204×753 -
desktop breakpoints apply). color sets the aluminum finish - a LAPTOP_COLORWAYS id
('skyblue', 'starlight', 'midnight', the Neo's 'citrus' and 'indigo') or any CSS
color. The Neo ('neo13') is the notchless one: its camera sits in the bezel above a
square-cornered panel.
Screens are display-only
Content on the glass renders live, but pointer events never reach it: clicks, scrolling and typing all belong to the orbit controls, so a drag anywhere - body, background, or screen - rotates the model.
That is deliberate, and it is what buys the mockup its looks. A screen is real DOM composited into a WebGL scene, and where that DOM sits in the stacking order decides how hardware can hide it. react-3d-mockups always stacks it under the canvas and masks it with the depth buffer, so anything in front of the screen covers it exactly, pixel for pixel: a laptop's keyboard hides the screen's reflection, a proud camera ring stands over a wrap, a bus's mirrors draw over the livery.
Lifting the DOM above the canvas is the only way to make it clickable, and it costs exactly that masking - nothing in the scene can visually cover DOM that sits on top of it. Hiding degrades to an all-or-nothing guess from sample rays, which is wrong in both directions: content shows through hardware that should hide it, and a mostly-visible screen can blank out entirely. Mockups exist to look right, so that trade is not offered.
If you need a genuinely usable embedded app, render it in the page next to the mockup rather than on it.
The screen renders in its own React root, and React context from above the mockup -
a theme, i18n, your router, a query client - is bridged into it, so screen content
reads the same providers the rest of your page does. It is kept out of the
accessibility tree and the tab order by default (screenAccessibility).
Exporting images and video
The screen is DOM composited by the browser, not pixels in the WebGL canvas, so
canvas.toDataURL() gives you devices with empty screens. Screenshot the element in a
real browser instead (Playwright's locator.screenshot() captures both layers), or use
Remotion for video. Recipes: Exporting images and video.
Virtual screen resolutions
Every variant's screen defaults to the real device's logical resolution (CSS px):
| Device | variant | Portrait | Landscape | Basis |
| --- | --- | --- | --- | --- |
| Galaxy S26 | s26 | 360×780 | 780×360 | 2340×1080 panel at ⅓ (3x) |
| Galaxy S26 Ultra | s26ultra | 384×833 | 833×384 | One UI default FHD+ render @ 450 dpi |
| Galaxy Z Fold 7 (open / folded) | fold7 | 820×910 / 360×835 | swapped | inner 2184×1968, cover 2520×1080 |
| Galaxy Z Fold 8 (open / folded) | fold8 | 1020×770 / 480×758 | swapped | inner 2448×1848 (natively landscape), cover 1248×1972 |
| Galaxy Z Fold 8 Ultra (open / folded) | fold8ultra | 820×910 / 360×835 | swapped | inner 2504×2256 on the Fold 7 grid, cover 2520×1080 |
| Galaxy Z Flip 7 (open / folded) | flip7 | 360×838 / 316×349 | swapped | main 2520×1080, cover 948×1048 |
| Galaxy Z Flip 8 (open / folded) | flip8 | 360×840 / 316×349 | swapped | main 2520×1080 at 6.9", cover 948×1048 |
| iPhone 17 | 17 | 402×874 | 874×402 | 2622×1206 @ 3x point grid |
| iPhone 17 Air | air | 420×912 | 912×420 | 2736×1260 @ 3x point grid |
| iPhone 17 Pro | pro | 402×874 | 874×402 | 2622×1206 @ 3x point grid |
| iPhone 17 Pro Max | promax | 440×956 | 956×440 | 2868×1320 @ 3x point grid |
| iPhone 18 Pro | 18pro | 402×874 | 874×402 | 2622×1206 @ 3x point grid |
| iPhone 18 Pro Max | 18promax | 440×956 | 956×440 | 2868×1320 @ 3x point grid |
| iPhone Duo (open / folded) | duo | 890×626 / 466×678 | swapped | inner 2670×1878 (natively landscape) and cover 1398×2034 @ 3x point grid |
| MacBook Air 13" (M5) | air13 | - | 1280×832 | 2560×1664 @ 2x default scaled |
| MacBook Air 15" (M5) | air15 | - | 1440×932 | 2880×1864 @ 2x default scaled |
| MacBook Pro 14" (M5) | pro14 | - | 1512×982 | 3024×1964 @ 2x default scaled |
| MacBook Pro 16" (M5) | pro16 | - | 1728×1117 | 3456×2234 @ 2x default scaled |
| MacBook Neo 13" | neo13 | - | 1204×753 | 2408×1506 @ 2x default scaled |
| iPad Pro 13" (M5) | ipadpro13 | 1032×1376 | 1376×1032 | 2752×2064 @ 2x point grid |
| iPad Pro 11" (M5) | ipadpro11 | 834×1210 | 1210×834 | 2420×1668 @ 2x point grid |
| iPad Air 13" (M4) | ipadair13 | 1024×1366 | 1366×1024 | 2732×2048 @ 2x point grid |
| iPad Air 11" (M4) | ipadair11 | 820×1180 | 1180×820 | 2360×1640 @ 2x point grid |
| iPad (A16) | ipad11 | 820×1180 | 1180×820 | 2360×1640 @ 2x point grid |
| Galaxy Tab S11 | tabs11 | 800×1280 | 1280×800 | 2560×1600 panel at ½ (xhdpi) |
| Galaxy Tab S11 Ultra | tabs11ultra | 924×1480 | 1480×924 | 2960×1848 panel at ½ (xhdpi) |
| Apple Watch Series 11 46mm | series11 | 208×248 | - | 416×496 @ 2x point grid |
| Apple Watch Series 12 46mm | series12 | 208×248 | - | 416×496 @ 2x point grid |
| Apple Watch Ultra 4 49mm | ultra4 | 211×257 | - | 422×514 @ 2x point grid |
| Galaxy Watch 8 44mm | watch8 | 240×240 | - | 480×480 round panel at ½ |
| Galaxy Watch 9 44mm | watch9 | 240×240 | - | 480×480 round panel at ½ |
| Galaxy Watch Ultra 2 47mm | watchultra2 | 249×249 | - | 498×498 round panel at ½ |
| Studio Display 27" | - | - | 2560×1440 | 5120×2880 @ 2x point grid |
<IPad> / <GalaxyTab> - the iPad lineup / Galaxy Tab S11 family
Shares the phones' screen/interaction API plus orientation, with a variant prop
('ipadpro13' | 'ipadpro11' | 'ipadair13' | 'ipadair11' | 'ipad11' | 'tabs11' |
'tabs11ultra'). Fully procedural and per-variant accurate: the Pro's camera pod
(wide lens, LiDAR, flash) and Pencil charging window, the Air's and standard iPad's
bare single lens with the Touch ID top button, back or edge Smart Connector dots and
speaker drill rows; protruding camera rings, quad speaker slots, gold pogo contacts
and (on the Ultra) the U-shaped display notch on the Galaxy Tabs; brand marks as real
vector geometry (Apple glyph, edge-aligned SAMSUNG wordmark) and model wordmarks on
the backs; landscape-edge front cameras, USB-C and machined edge buttons on all.
<AppleWatch> / <GalaxyWatch> - smartwatches · <StudioDisplay> - Studio Display-style
Both watches add bandColor and skip orientation. Every device draws its front camera unconditionally - a punch hole, Dynamic Island or notch is hardware, and it obstructs your layout here exactly as it would on the real panel. <AppleWatch> is the Apple Watch family ('series11' | 'series12' | 'ultra4'):
the Series' squircle case with the knurled Digital Crown, flush side button and sensor
back, worn on the seamless Solo Loop - which has no closure, so it takes no bandOpen -
and the Ultra 4's 49 mm titanium case with its raised lip, crown guard and orange Action
button, on its ridged, buckled Ocean Band.
<GalaxyWatch> is the Galaxy Watch family ('watch8' | 'watch9' | 'watchultra2'):
cushion case, round display on its dial puck, flat keys (the 47 mm titanium
Ultra 2 adds its orange Quick Button), BioActive puck, worn on a buckled
two-strap band that bandOpen lays out flat. The monitor puts the
2026 Studio Display's 27" 5K panel on its tilt stand - uniform bezel, centered
camera, the tight rear 2× Thunderbolt 5 + 2× USB-C slot cluster, the captive power
cord's circular recess framed by the stand's cable hole and, faithfully, no power
button.
Renderer-agnostic device specs (GALAXY_VARIANTS, IPHONE_VARIANTS, IPHONE_DUO_VARIANTS,
IPAD_VARIANTS, APPLE_WATCH_VARIANTS, GALAXY_WATCH_VARIANTS,
PHONE, IPHONE, LAPTOP… plus each object's region registry and stage framing) are
available from the react-3d-mockups/core subpath. It carries no 'use client'
directive, so a server component can import a spec for layout math.
Architecture
react-3d-mockups is one package in two layers. All device/object specs, region
registries, stage framing, geometry math and shared screen/stage behaviors live in a
renderer-agnostic core that depends on three and never on React; the components are
the layer that renders it through react-three-fiber.
The main entry re-exports a curated slice of the core (variants, colorways, size
types); the full core surface is available from react-3d-mockups/core, which carries
no 'use client' directive so a server component can import a spec for layout math.
See ARCHITECTURE.md
for the layering rule.
Docs & demos
Full documentation and live demos: area.is/react-3d-mockups. Source: github.com/area-is/react-3d-mockups
License
MIT © Ye Joo Park
