14 Pure CSS6 Light JSPublished 11 new designs · Updated on
A 3D scene is four properties working together — perspective on the parent, transform-style: preserve-3d so children keep their depth, the transform itself, and perspective-origin as the camera. These 20 hand-coded patterns cover that pipeline, why transform order changes the result, what silently flattens a scene, building a cube from six faces, translateZ versus scale, and z-fighting. Scoped under .d3-NN; drops into React, Vue, Svelte, or Astro.
Taking this style from one component to a whole product?
You just browsed 20 3D patterns. Untitled UI is the Figma UI kit 380,000+ designers use, with 10,000+ components and variants in Pro, plus a matching React library on Tailwind CSS. Figma Pro is a one-time purchase from $129.
The reference scene the rest of this collection borrows from: a viewing frustum declared with perspective on the parent, a preserve-3d world so children keep their own depth, a child transform, and perspective-origin acting as the camera. An axis gizmo and live property chips make each piece of the pipeline visible instead of implied.
Two ways to write perspective that are not equivalent, rendered side by side. perspective: 800px on a parent gives four panels one shared vanishing point and they read as a single scene; transform: perspective(800px) rotateY(30deg) gives each panel a private camera and they read as four unrelated objects. Both rows are labelled, and the ordering trap is built in.
perspective-origin moves the viewer, not the object. Five camera positions drive one unchanged block of geometry: the radio-style control switches the origin and the readout shows the value, so the shift in which faces you can see is clearly a change of viewpoint rather than a change of transform. No script.
translateX(120px) rotate(45deg) and rotate(45deg) translateX(120px) land in different places, because each function operates in the coordinate space the previous one left behind. Both orders run on identical elements with their travel traced, and the same lesson is then repeated in 3D where it bites harder.
Every transformed element has a back, and by default you look straight through to a mirrored copy of its front. backface-visibility: hidden stops that, which is what lets two elements stacked back to back behave as one two-sided object. Both states rotate side by side so the mirrored-text artefact is visible rather than described.
The most common 3D bug in CSS, catalogued. transform-style: preserve-3d lets children hold their own Z position, but a grouping property on an ancestor silently forces flattening: overflow, filter, opacity below 1, mask, clip-path and contain: paint. One correct scene, then the same scene broken six ways, each labelled with its culprit.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
The canonical construction, derived rather than hardcoded: six absolutely positioned faces, each rotated into place and pushed out by half the edge length. One custom property sets the cube's size and every translateZ follows from it through calc(), so resizing is a single edit instead of six.
transform-origin takes a third value, and that value decides whether a rotation is a spin or an orbit. The same rotateY runs on three identical panels — origin at the centre, at the left edge, and pushed back along Z — each labelled with its value, so the change from turning in place to swinging around a distant pivot is visible in one frame.
Both make an element look bigger and they are not interchangeable. scale multiplies rendered size on a flat plane; translateZ moves the element towards the camera so it grows because it is nearer — and parallaxes correctly against its neighbours. Two panels matched to identical apparent size, then the camera moves and the difference is unmistakable.
Two surfaces at the same Z depth give the renderer no way to decide which is in front, so it picks per frame and the result flickers — often only on some GPUs, which makes it maddening to reproduce. The failure is shown next to the three fixes that actually work: a sub-pixel Z separation, z-index in a flattened context, and restructuring so nothing is coplanar.
rotate3d(x, y, z, angle) rotates around an arbitrary axis vector, which rotateX, rotateY and rotateZ cannot express between them. Four wireframe plates turn around four different vectors with the vector drawn on each, and the readout names the fact that all of it compiles down to one matrix3d().
Items distributed evenly around a circle with one chained transform per item: rotateY(i × 360/n) then translateZ(radius). The radius is derived from item count and item width, so the panels never overlap and the ring resizes with the scene. Geometry, not a carousel — there is no active slide and nothing advances.
Five parallel planes at different translateZ values, with no rotation anywhere in the scene. Depth alone creates the stack: the camera sits off centre so the planes fan out, and each one is smaller than the one in front of it — which the last plane corrects with the compensating scale, so the maths is visible rather than asserted.
Three pricing cards that lean toward the cursor, with two background plates drifting at different rates behind the content and a glare that tracks the pointer. The tilt is not applied straight from the event — a requestAnimationFrame loop eases the current angle toward the target and back to rest, which is what separates a tilt that feels weighted from one that feels twitchy.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
A four-spread journal that turns on a spine hinge: transform-origin at the gutter turns rotateY into a page turn, each leaf carries a front and a back, and the script's real job is the stacking order — z-index recomputed on every turn so turned leaves fall behind the ones still to come. Prev and next buttons, click-a-half, and arrow keys all drive the same single index.
A six-faced cube used as a portfolio navigator, driven three ways: six preset buttons that fly to a named face, drag to orbit freely with the mouse or a finger, and an auto-rotate toggle running off requestAnimationFrame. All six faces derive their translateZ from one size token, so the cube is resized by changing a single number.
A mixer panel that reads as machined hardware while staying perfectly face on: depth comes from stacked translateZ plates and a shadow ladder, not rotation. Six faders are built from data and draggable, two VU meters run a twelve-segment LED ladder, and the transport buttons and routing switches drive a shared readout — so every control is a real button with real state.
The geometry of a ground plane: a grid rotated flat with rotateX, spacing chosen so the lines converge legibly, and a horizon placed by the camera rather than drawn by hand. The vanishing point is marked, the scroll is a single background-position animation, and there is no glow anywhere — the subject is where the lines go.
Four levels of 3D nested inside each other: a perspective root, a scene the pointer steers, three cards floating on their own keyframes, and a two-sided face that flips on hover — each level's transform composing with the ones above it. A gem built from layered elements sits inside the innermost context, and a generated particle field sits deliberately outside all of them.
Items placed on a helix: sin() and cos() of a per-item angle give X and Z, a linear step gives Y. Where the trigonometric functions are unsupported the same positions are read from precomputed custom properties declared first, so the helix is identical either way — the maths is a progressive enhancement, not a requirement.
FAQ
Frequently asked questions
How do you create a 3D effect in CSS?
Four properties working together, and the ordering of responsibility is what people get wrong. perspective goes on the PARENT, where it establishes a single viewing frustum shared by all its children — put it on the element you are rotating instead and every element gets its own vanishing point, so a row of rotated cards looks subtly wrong in a way that is hard to name. transform-style: preserve-3d goes on the element whose children need to keep their own depth, or those children flatten into their parent's plane. The transform itself does the rotation or translation. And perspective-origin acts as the camera position, defaulting to the centre of the parent. Demo 01 renders all four with an annotation panel naming each one and what it contributes, which is the reference the rest of the collection builds on.
Why is transform-style preserve-3d not working?
Almost always because an ancestor is silently flattening the scene. Several common properties force a flattened rendering context even when preserve-3d is declared correctly: overflow set to anything other than visible, any filter, opacity below 1, mask, clip-path, contain: paint, and some will-change values. The frustrating part is that nothing errors — the scene simply renders flat, so it looks like the transform is broken rather than the context. The one that bites most often is overflow: hidden added to a wrapper for an unrelated reason, such as clipping a scrollbar. If you need to clip a 3D scene, the clipping wrapper has to sit OUTSIDE the element carrying preserve-3d, never on it. Demo 06 shows a working scene, then the same scene with each culprit applied in turn and labelled.
Does the order of transform functions matter?
Yes, and it is not a subtlety — translateX(100px) rotate(45deg) and rotate(45deg) translateX(100px) put the element in genuinely different places. Each function operates in the coordinate space left by the one before it, so rotating first turns the axes that the translation then follows. Read the list left to right as a sequence of instructions to a pen: move, then turn, then move again along the new heading. The effect is easy to see in 2D and considerably stronger in 3D, where a rotation reorients all three axes and a following translateZ pushes along a direction you may not have intended. Demo 04 traces both orders on identical elements with the paths drawn, which makes the difference obvious in a way that reading the spec does not.
What is the difference between the perspective property and the perspective() function?
They look interchangeable and are not. perspective: 800px on a parent creates one shared viewing frustum, so every child is seen from the same camera and they share a vanishing point — which is what makes a row of rotated cards read as one scene. transform: perspective(800px) rotateY(30deg) applies a private perspective to that single element, so a row of them reads as several unrelated objects each viewed head-on. Neither is wrong; they answer different questions. Use the property when the elements belong to a shared space, and the function when one element needs its own depth independent of its siblings. One ordering trap comes with the function: perspective() must appear first in the transform list, because a perspective applied after a rotation has nothing left to foreshorten. Demo 02 renders a row both ways, labelled.
Why can I see mirrored text through my rotated element?
Because every transformed element has a back, and by default the browser shows it to you — a reversed rendering of the front face, text and all. backface-visibility: hidden stops that, and it is what makes the two-elements-stacked-back-to-back pattern behave like one two-sided object rather than a see-through card. The property has to be set on the faces themselves, not their container, which is a common mistake when the markup nests a wrapper between. It also interacts with flattening: if an ancestor has collapsed the 3D context, backface culling has nothing meaningful to act on. Demo 05 shows a rotating panel with the property on and off so the mirrored-text artefact is visible rather than described; the panel is deliberately plain, since flip-card design belongs to the css-flip-cards collection.
Is CSS 3D bad for performance?
The CSS is cheap to write and the runtime cost is real, which is an unusual combination worth being explicit about. Every 3D-transformed element is promoted to its own compositor layer, which is what makes the animation smooth — the transform runs on the GPU without repainting. The cost is memory: each promoted layer holds a texture, and a scene with fifty of them will stutter on a mid-range phone even though nothing in the CSS looks expensive. Keep the number of simultaneously-transformed elements modest, avoid promoting elements that never move, and be wary of will-change: transform left on permanently, which forces promotion whether or not the element is animating. Text is the other cost: a glyph rasterised inside a heavily transformed element is rendered at the wrong scale and blurs, so keep type upright or large enough to survive the transform.
How do I build a cube in CSS?
Six absolutely-positioned faces inside a container with transform-style: preserve-3d, each face rotated into place and then pushed outward by half the cube's edge length. For a cube of side s, every face needs translateZ(s / 2) after its rotation — front and back at 0 and 180 degrees on Y, left and right at ±90 on Y, top and bottom at ±90 on X. Deriving the offset from a single custom property rather than hardcoding it means the cube can be resized from one value, which is the difference between a demo and something reusable. Two traps: apply the rotation before the translateZ or the faces push along the wrong axis, and handle backface-visibility or you will see the interior of the far faces through the near ones. Demo 07 derives the whole net from one token.
How is this different from the flip card and tilt card collections?
Several collections sit adjacent and each owns a specific component built with 3D. css-flip-cards (23 demos) owns the flip card completely — cube unfolds, vertical-axis flips, click-to-flip, auto-resizing height, grid matrices. css-3d-tilt-hover-cards (15) and css-card-hover-effects (33) own pointer-driven tilt as a card treatment. css-image-carousel (22) owns the coverflow carousel. This collection owns the transform pipeline underneath all of them: how perspective, preserve-3d, transform order and origin actually work, and why a scene flattens or a rotation goes the wrong way. If you want a finished flip card, start with the flip-card collection. If your flip card is rendering flat and you cannot work out why, start here.