# 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 | preserve-3d |
# 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 oftransform
gives depth to that specific element - The
perspective
property 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); } }
1
2
3
4
5
6
7
8
9
10
11- 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); } }
1
2
3
4
5
6
7
8
9
10
11 - 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-origin
to another rotation point, for example:
#with div span {
animation: withPerspective 4s linear infinite;
transform-origin: right;
}
1
2
3
4
2
3
4
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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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
perspective
property comes in action: when we delete theperspective()
method from the child elements and replace it with theperspective
property on the parent (thediv
tag) 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 */
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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 thetransform
property of the element
# When to use the perspective
property?
- If two or more elements have to be transformed, use the
perspective
property - First create a scene container around those elements, e.g.
div.scene
, and set theperspective
property on this scene - Attention: the scene itself MAY NOT have any transformation properties!
# perspective-origin
- As you already know: the
transform-origin
property sets the reference point for thetransform
of a child element - The
perspective-origin
does about the same, but this time for thetransform-origin
property on the whole scene - So,
perspective-origin
determines the position from which you are looking at the whole scene - The values for
perspective-origin
are 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
90deg
along the X axis - The whole scene has a
perspective
of300px
- The animation changes the
perspective-origin
- The green dot is on the
perspective-origin
and 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-origin
property 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-style
property 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-style
on 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 */ }
1
2
3
4div.parent
is 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); } }
1
2
3
4
5
6
7
8- Inside
div.parent
are two child elements which have a differenttranslateZ()
value- The red box has to move
5rem
closer to the viewertransform: translateZ(5rem)
; - The green box has to move
8rem
closer to the viewertransform: translateZ(8rem)
;
- The red box has to move
span:first-child { ... transform: translateZ(5rem); } span:last-child { ... transform: translateZ(8rem); }
1
2
3
4
5
6
7
8- 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;
}
1
2
3
4
5
2
3
4
5
# Examples
# 3D figcaption
- Let's start from a finished transition example, in which a
figcaption
is rotated around the X axis (from90deg
= invisible to0
= visible) when hovering its enclosingfigure
- All you have to do to change the squeezed
figcaption
to 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);
...
}
1
2
3
4
5
2
3
4
5
<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.front
anddiv.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
180deg
on 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); }
1
2
3
4
- Step 2: Flip the card
- To flip the whole card, we rotate the front and the back
180deg
on the hover state of the card (div.scene
) - Because the back already starts from
180deg
onward, it continues to rotate to360deg
.front, .back { ... transition: 2s; } .scene:hover .front { transform: rotateY(180deg); } .scene:hover .back { transform: rotateY(360deg); }
1
2
3
4
5
6
7
8
9
10
- 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; }
1
2
3
4
5
- 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; }
1
2
3
4
5
6
7
8
9
10
11
- 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; }
1
2
3
4
5
6
- 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
2rem
on 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.front
and the parent for the text 'back' isdiv.back
The parent element fordiv.front
anddiv.back
isdiv.scene
h1 { margin-top: 3rem; color: firebrick; transform: translateZ(2rem); } .scene { ... perspective: 500px; transform-style: preserve-3d; } .front, .back { ... transform-style: preserve-3d; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- Translate the text by
# Flapping image
- Our scene contains two levels of nested elements, and they both contain their own animation
- The
figure
rotates 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; }
1
2
3 - Step 2: Let the image "flap"
- Add an alternating animation to the
img
tag (rotate between-40deg
and40deg
) - Set the
transform-origin
to thetop
of 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); } }
1
2
3
4
5
6
7
8
9
10
11 - Add an alternating animation to the
- Step 3: Rotate the figure
- Rotate the
figure
around 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); } }
1
2
3
4
5
6
7
8
9
10 - 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; }
1
2
3
4 - The image stops to flap because the figure has his own transform now!