The Base Fragrances homepage has six perfume bottles in a row. Drag the one in the middle and it spins, click a neighbour and it takes over. It’s real WebGL, not a video or a photo trick, and building it taught me a few things worth writing down.
The brief
A luxury fragrance brand needed a storefront that didn’t feel like a template, and the obvious centrepiece was the bottle itself. The catch: no 3D scan of the real bottle exists yet. So instead of waiting for one, I built the bottle from basic shapes in Three.js.
Building a bottle from primitives
The real bottle has faceted glass, so the body is an eight-sided prism. One detail matters more than it sounds: a prism with an even number of sides points an edge at the camera by default. Offsetting it by half a segment points a flat face at the camera instead, and that flat face is where the logo and name plate sit.
const FACETS = 8;
const FACE_OFFSET = Math.PI / FACETS; // centres a flat face toward the camera
new THREE.CylinderGeometry(r, r, height, FACETS, 1, false, FACE_OFFSET);
Inside the glass is a slightly smaller prism for the liquid, coloured per fragrance from the product data. Some accent colours (a near-black variant, for example) are too dark to read against the page, so the liquid’s lightness is lifted when needed. Without that, the bottle looked like an empty silhouette. On top sit a short shoulder, a metal ring and a wide dark cap.
A name plate that doesn’t look like a sticker
The brass name plate is drawn on a 2D canvas and used as a texture: a brushed-metal gradient, a light and dark bevel, and a soft shadow behind the lettering. Two things made it look sharp instead of printed:
- Resolution. Drawing it at 2× still looked soft on high-DPI screens, so it’s drawn at 4× and scaled down.
- Anisotropic filtering. The facet is usually seen at an angle, and without it the plate and logo blur. It costs almost nothing to turn on.
The logo also uses a lit material with an alpha cut-out rather than an unlit transparent one. That small change lets it catch the studio lighting like engraved metal instead of reading as a decal.
The glass effect that didn’t work
My first plan for the glass was Three.js’s physically based transmission, which refracts what’s behind it. It was the “correct” answer, and it failed in practice. Either it washed out each perfume’s liquid colour, which defeats the point of personalised bottles, or I turned it down until the colour came back and it stopped looking like glass at all.
So the body is simple alpha-blended glass, and the reflective environment goes where there’s no colour identity to lose: the metal cap and ring. That environment is a tiny custom “studio” — a few bright panels, like the softboxes in product photography — rendered once into a texture and shared by every bottle.
Making six bottles cheap enough to run
Each of the six slots has its own renderer ready to go, but only the centre bottle is live. The neighbours are static, scaled down and dimmed. Neighbours sit at roughly 66% and 48% scale with falling opacity, which also sells the depth.
Switching bottles doesn’t tear scenes down and rebuild them. It stops the old bottle’s drag handling, starts the new one’s, and there is only ever one animation loop running. That avoids a whole class of clean-up bugs.
The carousel is also data-driven: it reads the product list from the store’s API, so adding a perfume adds a bottle, with its colour and scent notes.
Not blocking the page
The Three.js module is around 1.2 MB, and setting up six scenes is real CPU work. Originally it ran at page load and held up the rest of the page for several seconds. It now starts when the section is near the viewport, plus a short delayed start after load so there’s no empty canvas flash when someone scrolls straight there. The library is also served from the site itself rather than a CDN, so there’s one less thing to fail.
Idle motion that doesn’t hide the label
A full idle spin shows the name plate to the camera only about once a minute, which is a poor deal for a product whose name is on the label. Instead the bottle sways gently either side of the label-facing angle. Dragging still spins it freely, and the sway resumes a couple of seconds after you let go.
Fallbacks
If WebGL isn’t available, the canvas is hidden and a static image takes its place. If the visitor has asked their system for reduced motion, the auto-rotation is switched off.
Takeaways
- Primitives are enough for a convincing product, and you don’t need a scan to start.
- Texture resolution and anisotropic filtering do more for perceived quality than extra geometry.
- Choose the look that serves the brand over the technique that’s physically “right”.
- Only one scene needs to be live at a time; keep the others static.
- Defer heavy setup so it never blocks the first screen.