Skip to content
Leverage AI

Free guide · from the Reel

Build a scroll-driven 3D real estate website with Claude Code

Paste the prompts below into Claude Code to build a scroll-driven website for a made-up tower called Velmora. It opens on a daylight rendering of a real tower. Hover a floor and only that floor's glass turns gold, stopping at the white balconies, and a card for that residence appears. Scroll, and the page zooms to floor 27 and pulls the tower apart there. The gold outline settles onto a 3D model of the apartment and fades, the model tilts so you see the rooms from above, and the camera drops into the living room, which turns into a photo-like image. It uses three.js and Lenis, both MIT, plus free fonts and a free upscaler. Beyond our Claude subscription we spent about 0.90 USD on an image model for the four room photos.

  • three.js + Vite
  • Hover any floor
  • Photo-real rooms

Published · Updated

Start building
Watch
the full scroll, 16.7 seconds
. The floor data and the 3D apartment are made in code. The four room images are renders of that apartment, redone as photos by an image model (Step 5).

What you need

  • Node.js 20.19+ or 22.12+, which Vite 8 requires. We used Node 22.23.
  • Claude Code, installed and signed in. It needs a paid Claude plan that includes Claude Code (Pro and Max do) or an Anthropic API account. The setup page has the install command; run claude once to sign in.
  • A browser. Claude also uses Playwright to take screenshots of its own work; it installs that itself.
  • An image of your building. A daylight rendering with sharp floor lines works best.
  • FFmpeg, only if you want to turn the recording in Step 8 into a video. We used 6.1.1.

You don't need GSAP or a 3D model of the tower. A Replicate account is optional: we used it for the room photos in Step 5, and without it the rooms stay plain renders.

The steps

Step 1: Make an empty folder and start Claude Code

bash
mkdir velmora && cd velmora
claude

Step 2: Paste the build prompt

Paste this prompt. Change the name, the floor count and the colours to suit your project. The list of sections matters most, because Claude builds them in that order.

text
Build me a premium scroll-driven website for a new luxury apartment tower. It's made up: call it Velmora, a 34-floor glass tower on a waterfront. Use Vite with three.js and Lenis for smooth scroll. Both are MIT; don't use GSAP, I want open-source libraries only.

As you scroll:
1. Hero: the tower at dusk, built in three.js, with warm lit windows and water in front. Hovering a floor highlights it and shows a label card (floor, residence name, beds/baths, size, available). No prices.
2. Scrolling zooms the camera into the facade.
3. Then one residence becomes a 3D floor plan that lifts apart floor by floor / wall by wall, with room labels.
4. Then the rooms: living, kitchen, bedroom, bath as full-screen photos with soft transitions between them and a room name caption. Use free Pexels or Unsplash photos, download them into the project and list every photo URL and its licence in LICENSES.md.
5. A contact section with a form, and a footer that says Velmora is a fictional demo.

It should feel like a high-end real estate site: dark navy, warm gold, a serif display font. Take screenshots with Playwright at 1680x1050 while you work and fix whatever looks off.

Our session also told Claude how to start Chromium on a server with no GPU (see "If something looks off"). On a normal laptop you can leave that out.

In our run, Claude did the following on its own:

  • installed three 0.187.0, lenis 1.3.27, vite 8.3.5 and playwright 1.64.1
  • searched Pexels, downloaded 16 candidate room photos, made a contact sheet and picked four (Step 5 later replaced them)
  • added self-hosted fonts (Cormorant Garamond and Jost, both OFL) through Fontsource
  • wrote the tower, the floor plan, the room fades and the hover cards as separate files in src/
  • wrote a screenshot script and took 16 screenshots down the page, then fixed the water reflection and pushed the skyline further back

The floor data (collections, beds, baths, sizes) lives in src/data.js. Claude also made the viewing form show a "nothing was sent" message instead of sending anything, though our prompt only asked for "a form". For your own demo, add this line to the prompt so the form can't mislead anyone: "The viewing form is a demo: on submit, send nothing and tell the visitor nothing was sent."

The first tower was built from boxes and shaders in src/tower.js. We sent one fix for it: at the end of the facade zoom the camera flew into a lit window and the screen went flat beige for half a second ("Stop the camera before it reaches the glass and start the fade to navy a bit earlier"). The bigger problem only showed when we watched the whole thing: the tower looked like a game render, not a building. Step 3 replaces it with an image of a real tower, which has no camera path through the windows, so you can skip that fix.

Step 3: Put in a real tower and outline every floor's glass

Our second version used a free night photo of a Miami tower. The feedback on it: the floors were too small, the night made it muddy, and the highlight has to sit exactly on the floor. So we switched to a daylight rendering: Riviera, 4988 Bennett Street in Burnaby, by Ledingham McAllister, from its project page on presalehomes.ca. It is not a free image: we asked the builder for permission, and they gave it, so the credit says "used with permission". If you use a real building, say next to the image that the residences and rooms are invented and that this is not the building's sales site. A footer line is too easy to miss. Ours now says it in one small line beside the tower on the first screen: "Demo site. Velmora, its homes and its rooms are invented. Tower image: Riviera by Ledingham McAllister, used with permission." It fades out as the zoom lands, so it never sits over the split or the 3D floor.

Put your image in the project folder and send this to Claude Code (we used Opus 5.5), with your file name in place of tower.png:

text
Replace the tower image with tower.png. Look closely: for each floor in the top half of the tower, draw the glass (the window layer) as an outline that leaves out the white balcony slabs and trim. Each face of glass is a near-rectangle in the image's perspective, so one floor can need several four-corner quads. Measure on the original pixels and store the quads per floor in hero.config.js. On hover, fill only those quads in gold with a thin bright edge, never the trim or the sky. Then draw every outline in alternating colours on 1:1 crops of the top, middle and lowest interactive floor, look at them, and fix any quad that spills onto trim or sky by more than one pixel. If the image is small, upscale it with a free local model for display, but keep measuring on the original pixels.

Claude worked through it in this order:

  • It upscaled the image with Real-ESRGAN (realesrgan-x4plus, BSD-3), which ran locally on the CPU through its ncnn build and took about 17 minutes on our server with no GPU. We compared it with plain Lanczos on crops: the glass and balconies came out sharp, with straight lines and no waxy patches, so we kept it. The page loads the 4352x5920 JPEG (2.8 MB) but all coordinates stay in original pixels.
  • To find the floors it used colour: glass has much more blue than red, and the white trim and its shadow have almost none, so a colour test separates them. Claude measured how steeply the floor lines run on each face, found they all head for two vanishing points, and used those to straighten each face so its floor lines run flat and can be read off one by one.
  • Each floor shows four bits of glass (a wide left face, two sides of a glass corner between the balcony columns, a narrow right face). The balconies hide where those faces meet, so Claude traced the floor lines around the hidden corners and checked that they landed on the measured ones.
  • On the left face a white slab edge runs along every third floor, switching between the left and right half. Claude split that face into parts and stopped each floor's glass at the slab. The result is 106 quads for floors 16 to 34 in src/hero.config.js.
  • Then a script counted trim pixels more than 1 px inside each quad. Three quads failed where a slab edge thickens near a corner, and were trimmed at that end. Looking at the crops found one more miss the script didn't: the crown's parapet sat about 3 px into the top floor on two faces, so that floor now starts lower there.

Every interactive floor outlined in alternating colours, middle of the tower, at 1:1 on the display image

The same check on the top floors and the lowest interactive floors.

Step 4: Make the stages one camera move

Our earlier version cut from the tower to a close-up photo of another building, faded to navy, then showed the floor plan. The reference site we were matching moves the camera the whole way. Send this:

text
The stages should be one continuous camera move, not cuts. Scroll zooms in and stops with the featured floor centred. Then split the image along that floor's slab lines: the floors above slide up and the floors below slide down, with a little blur and fade. As they move apart, fade out the floor's own strip of the image (its balcony tips are not part of the 3D floor), and fade out the 3D model's ceiling where it is, without lifting it, so the floor is open before the camera moves. Meanwhile morph the floor's gold outline into one rectangle that sits exactly on the 3D model's glass wall (project the wall's corners to the screen every frame, so it follows the camera), then fade the outline out as the 3D model takes over at the same place on screen. The model then tilts to an isometric view. Keep the model on screen while the residence card shows, then fly the camera down to eye level in the living room. Once the camera is below the ceiling, fade the ceiling and its downlights back in quickly, finishing before the living-room image starts to cross-fade in from that exact camera pose. Between rooms, dip to black for about a quarter of a second. Room images full-frame and bright, with only a small gradient under the room label. No navy fades and no cut to another building.

How the code does it:

  • The tower, the 3D apartment and the rooms now share one sticky section (15 screens tall), driven by one progress value. The timeline is a small table in src/main.js with a start and an end for each stage, so changing the pacing means changing a number.
  • For the split, src/hero.js puts the image in three layers with CSS clip-path polygons cut along the floor's two slab lines. The upper layer moves up and the lower one moves down by 62% of the screen height and fades out, while a blurred copy of the image fades in over them. The middle layer, the floor's own strip of the image, stays put and fades out while the halves leave, so its balcony tips and a white corner wedge are gone before the 3D floor takes over. The model is already behind it, posed to match.
  • To morph the outline, src/plan.js works out where the corners of the model's glass wall are on screen every frame, and src/hero.js moves floor 27's six gold pieces onto that rectangle, each to its own slice, with matching corners so nothing twists. Because the target is worked out from the 3D camera, the rectangle lands on the model's glass edges and stays there as the camera starts to move.
  • At the handover, the 3D camera starts with an 11-degree lens straight in front of the glass wall, at the distance where the 21 m wall covers the same 62% of the screen width as the floor did. Its ceiling has already faded out behind the image strip, so the camera tilts onto an open floor and no slab flies off. The model's glass starts gold and clears as the camera lifts to the isometric view.
  • After the residence card, the camera dives along a curve from the isometric view to the living-room pose in src/rooms.config.js. Once the camera is below the ceiling, the ceiling and its downlights fade back in together over about 0.3 seconds and are fully on just before the living-room image fades in over the 3D view from the same pose.

Step 5: Render the rooms from the same apartment, then make them look like photos

The four stock room photos came from four different homes and didn't match the floor plan. We removed them and asked for this:

text
Furnish the three.js apartment so each room has clear, real-looking furniture: a sofa, a dining table, a kitchen island, a bed, a bath. Use physically based materials, an environment light, soft shadows and warm lamps. Add a script that renders the living room, kitchen, bedroom and bath at 1920x1080 and saves the camera pose of each to poses.json. Use those renders as the room images, loaded from a config file, and make sure the living-room pose is the one the scroll dives to.

Claude rewrote src/plan.js with a linen sofa, boucle lounge chairs, a walnut dining table with six chairs under a brass pendant, an oak kitchen with a marble island and three stools, a bed with linen bedding and lamps, a tub, a double vanity and a shower. Floors use procedural oak planks and marble, lit by RoomEnvironment, a low sun with soft shadows and warm point lights. The bedroom became a corner room with glass on two sides so its render has a view. A hidden copy of the ceiling casts the sun's shadow, so sunlight still falls into the rooms only through the windows while the visible ceiling is faded out for the cutaway.

scripts/model-views.mjs opens the page with ?view=living (and so on), renders at 3840x2160 with ambient occlusion, scales down to 1920x1080 and saves the image and poses.json. Each still took 20 to 40 seconds on our server. The renders were clean, but they still looked like 3D renders.

The photo pass

So each render went through an image-to-image model once: Google's Nano Banana Pro on Replicate (google/nano-banana-pro), with the render as the input image, aspect_ratio 16:9, resolution 2K and PNG output. We ran it from a short Python script that calls Replicate's HTTP API with a REPLICATE_API_TOKEN; you can ask Claude Code to write one, or upload the render and paste the prompt on the model's page on Replicate. One prompt template covers all four rooms. The opening asks the model to keep everything in place, the middle names the room's materials, and the end sets the light. The living-room prompt:

text
Turn this 3D render into a real architectural interior photograph of a high-floor apartment in a new Vancouver-area tower. Keep the exact same camera position, lens, framing and perspective. Keep every wall, window mullion, ceiling edge and piece of furniture in exactly the same place, size and shape, so the photo lines up with the render pixel for pixel. Do not add, remove or move anything. Make the materials real: the back wall behind the sofa and floor lamp is a solid painted white wall with no windows, exactly as in the render; the only glass is the wall of windows on the right. Materials: wide-plank natural oak floor with fine grain, a cream boucle sofa and two boucle armchairs, a solid walnut dining table with upholstered walnut chairs, a brushed-brass linear pendant, white marble sideboard top, a real fiddle-leaf fig in a ceramic pot, a wool rug. Through the floor-to-ceiling glass, a clear daytime view over green parkland to the distant city and North Shore mountains. Soft natural daylight, gentle shadows, true-to-life colour, shot on a full-frame camera with a 24 mm lens, like a developer's show-suite photo. No people, no text, no watermark.

For the other rooms, swap the middle part for that room's furniture and materials. Our first try at the living room didn't have the "back wall ... no windows" sentence, and the model turned that solid wall into more glass. The first bathroom still looked like a render, so its second try opened the middle part with "this must look like a real photograph, not a 3D render: real light falloff, slight lens vignetting, natural imperfections." Six images in total (four, plus the two retries), each done in about 26 seconds, about 0.90 USD at 0.15 USD per 2K image.

The living room and kitchen as three.js renders (left) and after the photo pass (right)

Our images came back at 2752x1536, which is a little wider than 16:9 (1.792 against 1.778). We resized them to 1920x1080 without cropping, which squeezes them 0.8% across, because the living room has to line up with the 3D view it fades in from. On the living room, an edge match between the resized photo and the render peaked at a shift of 0 px, and in a 50/50 blend the table, chairs, sofa and pendant sat on top of each other. A crop would move the picture against the 3D view, and the room would jump at the cross-fade. This is the page halfway through that fade, from our recording:

Halfway through the cross-fade from the 3D view to the living-room photo: table, chairs, pendant and sofa line up

The prompt says "Do not add, remove or move anything", and the model still added things. Our kitchen photo has windows on the left and back walls that our apartment doesn't have, and the living room gained a second fig tree at the back left. The park, city and mountain views came from our prompt; the 3D apartment only has a plain sky. If a room changes too much, run it again with a sentence naming the part to keep, as we did for the living-room wall. The footer credits the rooms as an AI photo pass over our renders.

Step 6: Build it and check the build

bash
npm run build
npx vite preview

Claude had set the preview port to 5184 in vite.config.js, so ours opened at http://127.0.0.1:5184/. Use whatever address yours prints and scroll the whole page once. Our dist/ folder came to 5.8 MB: 1.5 MB of code and fonts, 2.8 MB for the tower image, 0.3 MB for its blurred copy and the backdrop, and 1.1 MB for the four room photos.

Step 7: Put it online for free

dist/ is plain static files, so any free static host works. We didn't upload ours, but we tested the one setting that catches people out.

By default Vite writes paths like /assets/index.js that start at the domain root (Vite docs on base). We copied dist/ into a subfolder and served it, the way a GitHub Pages project site would. The HTML loaded, but it asked for /assets/... and /photos/... at the root instead of inside the subfolder. Those requests got 404s, so the page stayed blank. Building with a relative base fixed it:

bash
npx vite build --base=./

After that every file loaded from the subfolder. We then put base: './' in vite.config.js so a plain npm run build does the same. Then pick a host:

  • Netlify Drop: drag the dist folder onto the page.
  • Cloudflare Pages: connect a Git repo, or upload dist directly.
  • GitHub Pages: put the contents of dist (not the folder itself) at the root of a branch such as gh-pages, then open the repo's Settings > Pages and choose that branch as the source. Use the --base=./ build.

Step 8: Record a smooth scroll video

optional

On our server with no GPU the 3D part ran at a few frames per second, so a live screen recording would have stuttered. We asked Claude Code for a recorder that renders the page one frame at a time:

text
Write scripts/record.mjs. Usage: node scripts/record.mjs <url> <outDir>, with the length in seconds from a DUR environment variable. Open the site in Playwright at 1728x1080, take over requestAnimationFrame and performance.now so the page only moves when we step it, scroll through the stage and into the contact section on a smooth curve and hold the contact section for the last 2 seconds, move a visible cursor over the tower floors for the first 2 seconds, and save one JPEG per frame at 60 fps as f00000.jpg, f00001.jpg and so on.
bash
npx vite preview --port 5184 --strictPort &
DUR=16.7 node scripts/record.mjs http://127.0.0.1:5184/ frames
ffmpeg -framerate 60 -i frames/f%05d.jpg -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart velmora-scroll.mp4

Our final recording, 1,002 frames (16.7 seconds of video), took 4 minutes 29 seconds on our server; the frames where the image splits are the slowest, at up to a second each. The video plays at a steady 60 fps, because the page clock moves exactly 1/60 s per frame.

What we ended up with

In the video of the finished page, watch the gold stop at the balconies, then follow floor 27's outline onto the 3D apartment and down into the living room. The viewing form at the end sends nothing.

Watch
the full scroll, 16.7 seconds
, or see the short loops:
tower hover
,
tower to 3D apartment
and
dive and rooms
.

If something looks off

The gold spills onto the balconies or the sky

The quads in src/hero.config.js are in the original image's pixels. If you cropped or resized the image after measuring, every number is off. Ask: "Draw every floor's quads in alternating colours on 1:1 crops of the top, middle and lowest floor and fix any quad that covers white trim or sky." On our tower the misses were where a slab edge thickens near a corner and where the crown's parapet meets the top floor.

Screenshots are slow or the 3D part is black in headless Chromium

On our server, which has no GPU, headless Chromium showed no WebGL until we launched it with --use-angle=swiftshader --enable-unsafe-swiftshader --ignore-gpu-blocklist. With those flags, a run of 17 screenshots down the page took about 2 minutes. We didn't test this on a laptop with a graphics card.

The 3D shadows are hard-edged after an update

In three.js 0.187 the WebGL renderer no longer has PCFSoftShadowMap. It logs "PCFSoftShadowMap has been removed. Using PCFShadowMap instead." (src/renderers/webgl/WebGLShadowMap.js) and draws hard-edged shadows. Set PCFShadowMap yourself and give the light a shadow.radius (we used 5) to soften the edges.

The recorder times out when the image splits

Our first version blurred the sliding floors with a CSS filter: blur(). On our server every screenshot from that point waited more than 30 seconds and Playwright gave up, even after we moved the blur onto screen-sized boxes. We baked the blur into a second, half-size copy of the image instead (riviera-hero-blur.jpg) and fade it in over the moving layers. The screenshots stopped timing out and the split looks the same.

The recording froze on the first frame

We first used Playwright's built-in fake clock (page.clock.install()). It froze the page's animation loop, and it also stalled Playwright's own screenshot call, which timed out. Replacing only requestAnimationFrame and performance.now inside the page fixed it.

The recording broke halfway

We recorded against the dev server (npm run dev) and then edited a file. Vite reloaded the page, and the recorder timed out. Record against npx vite preview instead, which serves the finished build and never reloads.

The living room jumps when it fades in

The living-room image has to show the room from the exact camera pose in src/rooms.config.js. If you replaced it with a new image, check that it was resized to the screen's shape, not cropped. Trimming even a few pixels off one side shifts every edge; a slight squeeze (ours was 0.8%) did not move anything we could measure, but check yours. Blend the image 50/50 with a screenshot of the 3D view at the end of the dive; if you see double edges, the image is off.

The ceiling pops in instead of fading

Our ceiling was meant to fade in at the end of the dive, but it appeared at full strength in a single frame, and earlier it flew off the model as a solid white slab instead of fading. The code turned the material's transparent flag off whenever the ceiling was fully on. three.js 0.187 bakes that flag into the shader the first time it compiles it (#define OPAQUE in src/renderers/webgl/WebGLProgram.js) and doesn't recompile when the flag changes later, so from then on the ceiling drew at full alpha whatever its opacity said. Leave transparent: true on anything you fade and change only opacity, or set material.needsUpdate = true after you flip the flag. Fade attached details such as downlights with the same value.

The page is blank on GitHub Pages

That's the root-path problem from Step 7. Rebuild with --base=./. The tower and room images are strings in src/hero.config.js and src/rooms.config.js, written without a leading slash (photos/riviera-hero.jpg), so the browser looks for them next to the page. Vite does not rewrite strings in JavaScript (it does handle images you import), so if grep -rn "'/photos" src/ finds anything in your source files, drop the leading slash or ask Claude to build the path from import.meta.env.BASE_URL.

Credits and licences

  • three.js 0.187.0, MIT.
  • Lenis 1.3.27, MIT.
  • Vite 8.3.5, MIT. Playwright 1.64.1, Apache-2.0.
  • Fonts: Cormorant Garamond and Jost via Fontsource 5.3.0, SIL Open Font License 1.1.
  • Upscaler: Real-ESRGAN ncnn-vulkan build, BSD-3-Clause.
  • Rendering: Riviera by Ledingham McAllister (used with permission).
  • Room images: AI photo pass over renders of our own three.js model, made with Nano Banana Pro (Google) on Replicate.
  • GSAP was not used. Its Standard No Charge licence is free but not open source.

Questions people ask

Do I need to know three.js or WebGL?

No. We described the sections in plain words and Claude Code wrote all the three.js, the scroll maths and the CSS. You do need to look at the result in a browser and say what looks wrong.

Why not GSAP ScrollTrigger? Most tutorials use it.

GSAP is free, including for commercial work, under Webflow's Standard No Charge licence. That licence is not open source and has a few limits, so we asked for Lenis (MIT) for smooth scrolling and plain JavaScript for the scroll timeline. If GSAP's licence is fine for you, you can ask for it instead.

How much did it cost?

About 0.90 USD on top of our existing paid Claude subscription. That was six images from Nano Banana Pro on Replicate (0.15 USD each at 2K) for the room photos. three.js, Lenis, Vite, Playwright, the two fonts and the Real-ESRGAN upscaler are free and open source. Skip the photo pass and the rooms stay plain renders at no cost. If you run Claude Code on API billing instead, you also pay for the tokens.

Why an image and not a 3D model of the tower?

Our first build drew the tower in three.js from boxes and shaders. It moved well but looked like a video game. A rendering has real light, glass and balcony detail, and the hover still works because each floor's glass is outlined on the image. The apartment further down is 3D.

Why make the room images from the 3D model instead of using stock photos?

Our earlier version used four stock photos from four different homes. They didn't match each other or the floor plan. Rendering the rooms from the same three.js apartment keeps each room's layout and furniture in line with the plan, and the cross-fade from 3D to image happens from one camera position. The plain renders looked like a video game, so an image model redid each one as a photo with the same framing. That pass added a few things the plan doesn't have, such as kitchen windows, so the photos give the feel of the rooms rather than an exact drawing of them.

Where can I host it for free?

The build is a folder of static files (5.8 MB for us), so any static host works: Netlify, Cloudflare Pages or GitHub Pages. Build with --base=./ if the site will live in a subfolder, such as a GitHub Pages project site.

Read it on your computer

Keep this guide for later.

We'll send you this page's link and the other free guides.

This guide's link

We email you this guide's link now, then one short AI news email a week. Unsubscribe any time.

Get every pro guideEvery pro guide in one $49 payment, as of October 2026.Follow on Instagram@leverageai_io, where our Reels and code words are.
Build a Scroll-Driven 3D Website with Claude Code | Leverage AI