How to generate a 3D phone model in the browser
A guide to a 3D phone model in the browser with Three.js and CSG: a beveled body, button and speaker cutouts, a Web Worker and fast subtraction of all holes.

To generate a 3D phone model in the browser, extrude a rounded rectangle to the device depth and then use boolean operations (CSG) to cut out the front panel, buttons, speakers and port. The whole device is then described by a handful of numbers in millimeters, and a new model means changing those numbers, not modeling in a 3D package.
This is useful wherever you need many similar devices or want users to adjust them: configurators, mockups, product previews. A real-world example is Ecranify, a tool for 3D mockups for App Store and Google Play screenshots that does not contain a single pre-made 3D model. Below is the process step by step, including a measurement that shows where the computation can be cut the most.
What you will need
- Three.js for the scene, cameras, lights and above all geometry. In Three.js, geometry is a mesh, a set of triangles.
- The three-bvh-csg library for boolean operations on meshes. It speeds them up with a spatial index (BVH, bounding volume hierarchy), so it does not test every triangle against every other one.
- A description of the device as data, for example JSON with body dimensions, corner radius, bevel size and a list of features on each side. Give all positions in millimeters from the device center.
Step 1: Extrude a rectangle into 3D
Draw a 2D rounded rectangle at actual size, for example 163.4 × 78 mm, and extrude it to a depth of 8.75 mm. You get a slab with sharp edges. No phone looks like that, so turn on a bevel, a rounded edge, during the extrusion as well. Three.js does both in one step.


One thing to watch: the bevel is added on the outside. If you need exact outer dimensions, shrink the 2D shape and the depth by the bevel size.
b = min(bevel, depth / 2)
shape = rounded rectangle(width - 2b, height - 2b, corner radius - b)
body = extrude(shape, depth - 2b, bevel b)
How smooth the curves are (tessellation) depends on the number of segments each curve is split into. More segments means a smoother shape and more triangles. This number later decides how fast the whole computation is, so we come back to it in step 7.
Step 2: Carve a recess for the display
CSG (constructive solid geometry) builds solids from simpler ones with three operations: union, subtraction and intersection. Subtraction works like a drill bit: one solid removes material from another.
A phone display is not flush with the frame. It sits in a shallow recess. To make it, create a second copy of the body that is 1.4 mm smaller, keep only its front part, 0.7 mm thick (an intersection with a large box), and subtract that part from the body. Then place a dark display surface into the recess.


Two practical details:
- The intersection box has to be really large. It only needs to clip depth, but in width and height it must exceed the whole device. Do not derive it from the depth: 20 times the depth, for example, is less than the height of a thin, tall phone, and an uncut frame is left at the top and bottom.
- Scale the cutter up by a few tenths of a percent. When two solids share exactly the same face, CSG cannot tell which side is inside. A scale of 1.002 prevents that and is invisible to the eye.
Step 3: Prepare side features as drill bits
Every feature on the side follows the same recipe with a different drill bit. A button, for example, is made from three solids: a rounded rectangle slightly deeper than the button is subtracted from the frame, the edge of the opening gets a small chamfer, and a slightly smaller solid that protrudes a little from the frame goes into the opening.


Speakers are a row of small cylinders, the port is a rounded rectangle. Put something dark into every opening: a thin disc into the grille, a plate into the port. An empty hole in a thin shell looks like a bug, a hole with a dark insert looks like hardware.

for each feature in the side feature list:
bit = feature shape (cylinder, rounded rectangle, ...)
place the bit on the edge at its position in mm
add the bit to the list of cuts
if the feature has an insert: add the insert to the scene
Adding a button is then a data entry. Features add up quickly: an iPad Pro 13″ has 58 speaker holes, 4 buttons, 2 antennas and a port, more than 60 subtractions from a single body.
Step 4: Do not drill front details
The camera cutout, the island at the top of the display and the home indicator are flat. Cutting geometry for them is wasted work. Apply them as decals instead, images projected onto a surface. Project them onto a hidden plane covering the whole front and position them in the same millimeters as everything else. The exception is the round home button of older iPhones. It has real depth you can see from the side, so it goes through CSG.
Step 5: Move the work into a Web Worker
JavaScript in the browser runs on a single thread. The same thread computes, paints the page and responds to clicks. While CSG runs, the page freezes, and a spinner will not help, because it needs that same thread too.
So run the whole device build in a Web Worker, a separate background thread. The worker receives the device description, returns finished vertex arrays, and the page stays smooth. When a parameter changes, terminate the old worker and start a new one, so it does not keep computing an outdated state.
The worker result takes a moment, so add an instant preview. Show the feature being edited as a blue translucent "ghost", a simple box or cylinder with a few dozen triangles that can be built on the main thread right away. When the worker finishes, the ghost disappears.
Step 6: Subtract all holes at once
The most direct approach is to subtract features one after another: hole 1 from the body, then hole 2 from the result, and so on. But every subtraction rebuilds the spatial index of the whole body, which is the most expensive part of the computation. With dozens of features the time grows very fast.
It is better to collect all drill bits for a given solid first, merge them into one mesh and subtract once.
cuts = []
for each feature: add its bit to cuts
merged = merge all cuts into one mesh
body = body minus merged
if merging fails: subtract cuts one by one
Measured on a simplified copy of the pipeline (Three.js 0.170, three-bvh-csg 0.0.17, Node 22, a cloud server with 2 Intel Xeon 2.1 GHz cores, median of 5 runs):
| Solid | Holes | One at a time | All at once |
|---|---|---|---|
| phone, 17,676 triangles | 25 | 2.47 s | 0.26 s |
| tablet, 17,676 triangles | 58 | 5.55 s | 0.89 s |
| tablet, 68,108 triangles | 58 | 22.8 s | 3.1 s |
One condition: merging and subtracting once gives the same result as subtracting one by one only if the drill bits do not overlap. Features sit in different places, so it usually holds. But if a user drags two features on top of each other, a small artifact can appear where they overlap.
Step 7: Keep the segment count as a parameter
Tessellation that is fine for display is often needlessly dense for CSG. With 64 segments per corner and 32 for the bevel, the body has 68,108 triangles. Half of that (32 and 16) is invisible on a 2 mm edge, but the body has only 17,676 triangles, and the third row of the table shows what that does to the time.


Give small features with a radius of up to 2 mm (speaker holes, screws) half the segments, gallery thumbnails even fewer. So do not hard-code quality as a constant. Make it a build parameter.
Rendering on a server without a GPU
If you need to render the same scene on a server too, for example as an export backup, it runs in headless Chrome, which draws in software because there is no graphics card. Every inefficiency is multiplied there. In Ecranify, the heaviest tablet hit the 15-minute limit without the changes above. With merged cuts, smaller textures, lower tessellation and a cache of finished geometry keyed by a hash of the device description, a render takes about half a minute and a repeat one about 5 seconds. The details, with interactive demos, are in the article on ecranify.com.
If you are solving a similar 3D problem in the browser or need a custom app, get in touch.


