3D Transforms (optional)
11/14/25About 12 min
3D Transforms (optional)
NEED-TO-KNOW
- Only 2D transforms are the subject matter to be mastered for this course
- 3D transforms are NOT PART OF THE SUBJECT MATTER to be mastered for this course, but they may challenge you or inspire you for future projects
REMARKS
- The content on this page requires quite a bit of spatial insight
- In order to demonstrate some 3D properties and methods, we make use of transitions and animations
- First go through the pages on 2D Transforms and Animations before you read on
- The 2D and 3D transform methods and transform-related properties are:
| 2D and 3D | 3D only |
|---|---|
translate(unit ,[unit]),translateX(unit), translateY(unit) | translateZ(unit), translate3d(tX, tY, tZ) |
rotate(angle), rotateX(angle), rotateY(angle) | rotateZ(angle), translate3d(nr, nr, nr, angle) |
scale(nr [, nr]), scaleX(nr), scaleY(nr) | scaleZ(nr), scale3d(sX, sY, sZ) |
skew(angle), skewX(angle), skewY(angle) | |
transform-origin: unit, unit | perspective-origin: unit, unit |
| `backface-visibility: visible | hidden` |
perspective(unit) (on element itself) | |
perspective: unit (on parent element) | |
| `transform-style: flat |
translateZ() and translate3d()
translateZ(): moves an element on the Z axis- Positive arguments brings the object closer to you
- Negative arguments pushes the object further away from you
translate3d()is a shorthand fortranslateX(),translateZ()andtranslateZ()- For example:
translate3d(20px, 0, 3rem)=translateX(20px) translateY(0) translateZ(3rem)
- For example:
REMARK
translateZ()doesn't do anything at all until you add some kind of perspective- You can try this on our interactive transform page
scaleZ() and scale3d()
scaleZ(): scales an element up or down along the Z axis- Arguments larger than 1 scales the object up (zoom in)
- Arguments smaller than 1 scales the object down (zoom out)
scale3d()is a shorthand forscaleX(),scaleZ()andscaleZ()- For example:
scale3d(1, 1.5, .7)=scaleX(1) scaleY(1.5) scaleZ(.7)
- For example:
REMARK
scaleZ()doesn't do anything at all until you add some kind of perspective- You can try this on our interactive transform page
rotateZ() and rotate3d()
rotateZ(): rotates an element around the Z axisrotate3d()is specified by three numbers for the X, Y and Z axes and one angle for the rotation
REMARKS
- The
rotate3d()property is NOT a list of X, Y and Z rotations liketranslate3d()andscale3d() - The three arguments together form a matrix from which ONE axis is calculated (the details of this calculation are beyond the scope of this page) around which the fourth argument (the angle) will rotate
perspective() method
- Perspective is an important feature to get a natural sense of depth, especially when rotating over one of the axes
- The perspective defines how far your eyes are away from the object
- A larger number means that you are further away from the object and the perspective looks smaller
- A smaller number means that you are closer to the object and the perspective looks larger
- There are two ways to generate some depth:
- The
perspective()method as a value oftransformgives depth to that specific element - The
perspectiveproperty gives depth on a total scene (that is, on a group of child elements)
- The
- Remember that when we use
rotateX()androtateY()to rotate along the X and/or Y axis, it looks like the elements are squeezed because, by default, they have no depth - By adding the
perspective()method in front ofrotateX()androtateY(), you enter the 3D space for this specific element and the rotation looks more natural - In the example below:
- The left animation has no
perspective()method
#without div span { animation: withoutPerspective 4s linear infinite; } @keyframes withoutPerspective { to { transform: rotateX(0); } to { transform: rotateX(360deg); } }- The right animation has the
perspective(200px)method IN FRONT OF the rotation method
#with div span { animation: withoutPerspective 4s linear infinite; } @keyframes withPerspective { to { transform: perspective(200px) rotateX(0); } to { transform: perspective(200px) rotateX(360deg); } } - The left animation has no
<section id="without"> <div> <span>without perspective</span> </div> </section> <section id="with"> <div> <span>with perspective</span> </div> </section>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; margin: 2rem; display: flex; flex-wrap: wrap; justify-content: center; }
section { display: flex; flex-wrap: wrap; }
div { border: 1px solid darkgray; background-color: rgba(169, 169, 169, .2); display: inline-block; margin: 1rem; }
span { display: inline-block; width: 150px; line-height: 150px; font-size: .8rem; text-align: center; border: 1px solid #000; background-color: rgba(248, 104, 52, 0.6); }
#without div span { animation: withoutPerspective 4s linear infinite; }
@keyframes withoutPerspective { from { transform: rotateX(0); } to { transform: rotateX(360deg); } }
#with div span { animation: withPerspective 4s linear infinite; /* transform-origin: right; */ }
@keyframes withPerspective { from { transform: perspective(200px) rotateX(0); } to { transform: perspective(200px) rotateX(360deg); } }
Exercises
- Play with the different perspective values, e.g.
perspective(500px),perspective(100px) - Change the rotation axis from
rotateX()torotateY() - Change the
transform-originto another rotation point, for example:
#with div span {
animation: withPerspective 4s linear infinite;
transform-origin: right;
}Note that, by specifying only one value for transform-origin, the other value defaults to center
perspective property
- Sometimes you have one object that contains two or more child elements (let's call it a "scene")
- When using the
perspective()method on every child element inside the "scene", this doesn't look very natural at all ...
span {
...
animation: rotate 4s linear infinite;
transform-origin: center;
}
div:hover span {
animation-play-state: paused;
}
@keyframes rotate {
from {
transform: perspective(500px) rotateY(0);
}
to {
transform: perspective(500px) rotateY(360deg);
}
} <div> <span>1</span> <span>2</span> <span>3</span> <span>4</span> <span>5</span> <span>6</span> <span>7</span> <span>8</span> <span>9</span> </div>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; text-align: center; height: 100vh; display: flex; align-content: center; justify-items: center; }
div { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; width: 80vmin; height: 80vmin; padding: 0 1%; margin: auto; border: 1px solid darkgray; background-color: rgba(169, 169, 169, .2); }
span { height: 30%; flex: 0 0 30%; font-size: 2rem; border: 1px solid #000; background-color: rgba(248, 104, 52, 0.6); animation: rotate 4s linear infinite; transform-origin: center; }
div:hover span { animation-play-state: paused; }
@keyframes rotate { from { transform: perspective(500px) rotateY(0); } to { transform: perspective(500px) rotateY(360deg); } }
- That's where the
perspectiveproperty comes in action: when we delete theperspective()method from the child elements and replace it with theperspectiveproperty on the parent (thedivtag) we get a more beautiful 3D perspective for the entire scene - Now the rotation on the whole scene looks very natural
div {
...
perspective: 500px; /* add perspective property for the whole scene */
}
span {
...
animation: rotate 4s linear infinite;
transform-origin: center;
}
div:hover span {
animation-play-state: paused;
}
@keyframes rotate {
from {
transform: rotateY(0); /* perspective() methode deleted */
}
to {
transform: rotateY(360deg); /* perspective() methode deleted */
}
} <div> <span>1</span> <span>2</span> <span>3</span> <span>4</span> <span>5</span> <span>6</span> <span>7</span> <span>8</span> <span>9</span> </div>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; text-align: center; height: 100vh; display: flex; align-content: center; justify-items: center; }
div { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; width: 80vmin; height: 80vmin; padding: 0 1%; margin: auto; border: 1px solid darkgray; background-color: rgba(169, 169, 169, .2); perspective: 500px; }
span { height: 30%; flex: 0 0 30%; font-size: 2rem; border: 1px solid #000; background-color: rgba(248, 104, 52, 0.6); animation: rotate 4s linear infinite; transform-origin: center; }
div:hover span { animation-play-state: paused; }
@keyframes rotate { from { transform: rotateY(0); } to { transform: rotateY(360deg); } }
perspective() vs perspective
When to use the perspective() method?
- If only one element has to be transformed, use the
perspective()method within thetransformproperty of the element
When to use the perspective property?
- If two or more elements have to be transformed, use the
perspectiveproperty - First create a scene container around those elements, e.g.
div.scene, and set theperspectiveproperty on this scene - Attention: the scene itself MAY NOT have any transformation properties!
perspective-origin
- As you already know: the
transform-originproperty sets the reference point for thetransformof a child element - The
perspective-origindoes about the same, but this time for thetransform-originproperty on the whole scene - So,
perspective-origindetermines the position from which you are looking at the whole scene - The values for
perspective-originare exactly the same as fortransform-origin- The default value is also
center center
- The default value is also
- In the example below:
- All child elements are rotated
90degalong the X axis - The whole scene has a
perspectiveof300px - The animation changes the
perspective-origin - The green dot is on the
perspective-originand represents the view point from where you are looking at the perspective
- All child elements are rotated
<div> <span>1</span> <span>2</span> <span>3</span> <span>4</span> <span>5</span> <span>6</span> <span>7</span> <span>8</span> <span>9</span> </div>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; text-align: center; height: 100vh; display: flex; align-content: center; justify-items: center; }
div { background-image: url(https://itf-web-essentials.netlify.app/assets/dot.svg); background-repeat: no-repeat; background-size: 1rem 1rem; background-position: 0% 0%; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; width: 80vmin; height: 80vmin; padding: 0 1%; margin: auto; border: 1px solid darkgray; background-color: rgba(169, 169, 169, .2); perspective: 300px; animation: moveOrigin infinite 10s; }
span { height: 30%; flex: 0 0 30%; font-size: 2rem; border: 1px solid #000; background-color: rgba(248, 104, 52, 0.6); transform: rotateX(90deg); /* transform-origin: left; */ }
@keyframes moveOrigin { 0% { perspective-origin: 0% 0%; background-position: 0% 0%; } 25% { perspective-origin: 100% 0%; background-position: 100% 0%; } 33% { perspective-origin: 100% 33%; background-position: 100% 33%; } 50% { perspective-origin: 50% 50%; background-position: 50% 50%; } 66% { perspective-origin: 66% 100%; background-position: 66% 100%; } 75% { perspective-origin: 10% 90%; background-position: 10% 90%; } 100% { perspective-origin: 0% 0%; background-position: 0% 0%; } }
Exercises
- Enable the
transform-originproperty and with some the different settings, e.g.top,bottom, ... - Modify the value of
rotateX()and look what happens - Rename the property
rotateX()torotateY()and look what happens
transform-style
- The
transform-styleproperty determines whether the DIRECT child elements are flattened in its 2D plane or shown in a 3D space - By default, all direct children are rendered in a flat, 2D plane (
transform-styleon parent is defaultflat) - To allow all direct children to be transformed on the Z axis (
translateZ(),rotateZ()and/orscaleZ()) , you have to EXPLICITLY set the property of the PARENT element totransform-style: preserve-3d - In the example below:
- Because we are going to transform several elements, we are going to use a scene with a global perspective
div.scene { ... perspective: 2000px; /* perspective for the whole scene */ }div.parentis the first element inside the scene and rotates along its Y axis
div.parent { ... animation: rotate 5s linear infinite; } @keyframes rotate { from { transform: rotateY(0); } to { transform: rotateY(360deg); } }- Inside
div.parentare two child elements which have a differenttranslateZ()value- The red box has to move
5remcloser to the viewertransform: translateZ(5rem); - The green box has to move
8remcloser to the viewertransform: translateZ(8rem);
- The red box has to move
span:first-child { ... transform: translateZ(5rem); } span:last-child { ... transform: translateZ(8rem); }- As you can see, the two child elements are flattened in their parent element (
div.parent) and theirtranslateZ()methods don't work!
<div class="scene"> <div class="parent"> div.parent <span>translateZ(5rem)</span> <span>translateZ(8rem)</span> </div> </div>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; margin: 3rem; }
div.scene { outline: 1px solid whitesmoke; width: 300px; height: 300px; margin: auto; display: flex; justify-content: center; align-items: center; perspective: 2000px; /* perspective for the whole scene */ }
div.parent { width: 90%; height: 90%; position: relative; border: 1px solid darkgray; background-color: rgba(101, 176, 251, 0.2); animation: rotate 5s linear infinite; /* transform-style: preserve-3d; */ }
@keyframes rotate { from { transform: rotateY(0); } to { transform: rotateY(360deg); } }
span { position: absolute; text-align: center; border: 1px solid darkgray; font-size: .8rem; }
span:first-child { top: 20px; left: 20px; width: 200px; line-height: 200px; background-color: rgba(251, 101, 101, 0.2); transform: translateZ(5rem); }
span:last-child { top: 50px; left: 50px; width: 250px; line-height: 100px; background-color: rgba(52, 248, 117, 0.6); transform: translateZ(8rem); }
- All you have to do to fix this behavior is enabling the 3D space on the parent element
div.parent {
...
animation: rotate 5s linear infinite;
transform-style: preserve-3d;
}Examples
3D figcaption
- Let's start from a finished transition example, in which a
figcaptionis rotated around the X axis (from90deg= invisible to0= visible) when hovering its enclosingfigure - All you have to do to change the squeezed
figcaptionto a nice, foldable version, is adding some perspective to the transformation- Add
perspective(300px)as the first method to the transformation and change the start rotation to60deg
- Add
figcaption {
...
transform: perspective(300px) rotateX(60deg);
...
} <figure> <img src="https://picsum.photos/id/1080/300/300" alt="Strawberries"> <figcaption>Strawberries</figcaption> </figure>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; padding: 1rem; text-align: center; }
figure { border: 1px solid darkgray; line-height: 0; display: inline-block; position: relative; }
figcaption { position: absolute; bottom: 0; left: 0; width: 100%; font-size: 2rem; line-height: 1.5; background-color: rgba(255, 255, 255, 0.3); color: white; transform: rotateX(90deg); /* change to: perspective(300px) rotateX(60deg) */ transform-origin: bottom center; transition: transform .5s; }
figure:hover figcaption { transform: rotateX(0); }
Flip card
- The flip card (
div.scene) contains two children (div.frontanddiv.back) - By default, the front and the back of the card stand beneath each other
<div class="scene"> <div class="front"> <h1>Front</h1> </div> <div class="back"> <h1>Back</h1> </div> </div>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; padding: 1rem; text-align: center; height: 100vh; display: flex; justify-content: center; align-items: center; }
h1 { margin-top: 3rem; color: firebrick; /* transform: translateZ(2rem); */ }
.scene { /* outline: 1px solid lightgray; */ /* width: 200px; height: 300px; position: relative; */ /* perspective: 500px; */ /* transform-style: preserve-3d; */ }
.front, .back { width: 200px; height: 300px; border: 1px solid #000; box-shadow: 1rem 1rem 2rem rgba(0, 0, 0, .3); /* transition: 2s; */ /* backface-visibility: hidden; */ /* position: absolute; top: 0; left: 0; */ /* transform-style: preserve-3d; */ }
.front { background-image: url(https://picsum.photos/id/1028/200/300); }
.back { background-image: url(https://picsum.photos/id/1028/200/300?grayscale); /* transform: rotateY(180deg); */ }
.scene:hover .front { /* transform: rotateY(180deg); */ }
.scene:hover .back { /* transform: rotateY(360deg); */ }
- Step 1: Rotate the back of the card
- At the end only the front is visible in the default state and the back is visible on the hover state
- We have to rotate the back by
180degon the Y axis, to avoid that the text on the reverse side is mirrored in the hover state.back { background-color: rgba(128, 201, 240, 0.6); transform: rotateY(180deg); }
- Step 2: Flip the card
- To flip the whole card, we rotate the front and the back
180degon the hover state of the card (div.scene) - Because the back already starts from
180degonward, it continues to rotate to360deg.front, .back { ... transition: 2s; } .scene:hover .front { transform: rotateY(180deg); } .scene:hover .back { transform: rotateY(360deg); }
- To flip the whole card, we rotate the front and the back
- Step 3: Disable the backface
- Only the part of the card that is not mirrored may remain visible
- If we do not do this, we will always see the back when we put the front and the back on top of each other
.front, .back { ... transition: 2s; backface-visibility: hidden; }
- Step 4: Position the front and the back on top of each other
- Position the front and the back absolutely inside the card container
.scene { width: 200px; height: 300px; position: relative; } .front, .back { ... position: absolute; top: 0; left: 0; }
- Position the front and the back absolutely inside the card container
- Step 5: Add perspective to the scene
- All you have to do to change the squeezed rotation to a nice 3D version, is adding some perspective to the whole scene and not to the individual elements
.scene { width: 200px; height: 300px; position: relative; perspective: 500px; }
- All you have to do to change the squeezed rotation to a nice 3D version, is adding some perspective to the whole scene and not to the individual elements
- Final step: Put the text in front of the card
- Translate the text by
2remon the Z axis to place it in front of the rest of the card - By default, the Z axis is rendered inside a flat surface, that's why we have to explicitly enable 3D on all its parent elements
- The parent element for the text 'front' is
div.frontand the parent for the text 'back' isdiv.back
The parent element fordiv.frontanddiv.backisdiv.sceneh1 { margin-top: 3rem; color: firebrick; transform: translateZ(2rem); } .scene { ... perspective: 500px; transform-style: preserve-3d; } .front, .back { ... transform-style: preserve-3d; }
- Translate the text by
Flapping image
- Our scene contains two levels of nested elements, and they both contain their own animation
- The
figurerotates around the X axis - The
img, inside thefigure, "flaps" on the top of thefigure
- The
<div class="scene"> <figure> <img src="https://picsum.photos/id/855/300/200" alt=""> <figcaption>Photo @ Rodion Kutsaev</figcaption> </figure> </div>
* { margin: 0; padding: 0; box-sizing: border-box; }
html { font-size: 16px; }
body { font-family: Verdana, Geneva, sans-serif; line-height: 1.5; height: 100vh; display: flex; justify-content: center; align-items: center; font-weight: bold; }
.scene { /* perspective: 1000px;*/ }
figure { text-align: center; width: 300px; margin: auto; border: 1px solid #000; border-top-width: 1rem; /* transform-style: preserve-3d; */ }
.scene figure { /* animation: rotateFigure 10s linear infinite; */ }
.scene:hover figure { /* animation-play-state: paused; */ }
@keyframes rotateFigure { from { transform: rotateY(0); } to { transform: rotateY(360deg); } }
img { vertical-align: middle; width: 100%; height: auto; /* animation: flapImage 700ms ease-in-out infinite alternate; transform-origin: top; */ }
@keyframes flapImage { from { transform: rotateX(-40deg); } to { transform: rotateX(40deg); } }
- Step 1: Add perspective to the scene
.scene { perspective: 1000px; } - Step 2: Let the image "flap"
- Add an alternating animation to the
imgtag (rotate between-40degand40deg) - Set the
transform-originto thetopof the image
img { vertical-align: middle; width: 100%; height: auto; animation: flapImage 700ms ease-in-out infinite alternate; transform-origin: top; } @keyframes flapImage { from { transform: rotateX(-40deg); } to { transform: rotateX(40deg); } } - Add an alternating animation to the
- Step 3: Rotate the figure
- Rotate the
figurearound his Y axis - Pause the animation on the hover state of the scene
.scene figure { animation: rotateFigure 10s linear infinite; } .scene:hover figure { animation-play-state: paused; } @keyframes rotateFigure { from { transform: rotateY(0); } to { transform: rotateY(360deg); } } - Rotate the
- Final step: Preserve 3D on the
figure- The image stops to flap because the figure has his own transform now!
(Remember that child elements are rendered on a flat pane by default) - Enable 3D on the
figure
figure { ... transform-style: preserve-3d; } - The image stops to flap because the figure has his own transform now!