CSS 3D Transforms can be used in plenty of creative ways, particularly if combined with CSS Transitions! Today’s nugget is a good example of how to use CSS to create a parallelepiped, whose faces are different projects. A filter on top of the page triggers the 3D rotations that reveal new projects.
Images: Unsplash
Creating the structure
The HTML structure is composed of two main elements: a nav.cd-3d-portfolio-navigation
for the top projects navigation and a div.projects
wrapping the portfolio projects. Inside the div.projects
, three unordered lists (ul.row
) are used to create the three rotating parallelepipeds.
<div class="cd-3d-portfolio">
<nav class="cd-3d-portfolio-navigation">
<div class="cd-wrapper">
<h1>3D Portfolio Template</h1>
<ul>
<li><a href="#0" class="selected">Filter 1</a></li>
<li><a href="#0">Filter 2</a></li>
<li><a href="#0">Filter 3</a></li>
</ul>
</div>
</nav> <!-- .cd-3d-portfolio-navigation -->
<div class="projects">
<ul class="row">
<li class="front-face selected project-1">
<div class="project-wrapper">
<div class="project-image">
<div class="project-title">
<h2>Project 1</h2>
</div>
</div> <!-- .project-image -->
<div class="project-content">
<!-- project content here -->
</div> <!-- .project-content -->
<a href="#0" class="close-project">Close</a>
</div> <!-- .project-wrapper -->
</li>
<li class="right-face project-2">
<div class="project-wrapper">
<div class="project-image">
<div class="project-title">
<h2>Project 2</h2>
</div>
</div> <!-- .project-image -->
<div class="project-content">
<!-- project content here -->
</div> <!-- .project-content -->
<a href="#0" class="close-project">Close</a>
</div> <!-- .project-wrapper -->
</li>
<li class="right-face project-3">
<div class="project-wrapper">
<div class="project-image">
<div class="project-title">
<h2>Project 3</h2>
</div>
</div> <!-- .project-image -->
<div class="project-content">
<!-- project content here -->
</div> <!-- .project-content -->
<a href="#0" class="close-project">Close</a>
</div> <!-- .project-wrapper -->
</li>
</ul> <!-- .row -->
<ul class="row">
<!-- projects here -->
</ul> <!-- .row -->
<ul class="row">
<!-- projects here -->
</ul> <!-- .row -->
</div><!-- .projects -->
</div>
Adding style
Each ul.row
has a height equal to one-fourth of the viewport height and is translated along the Z-axis of half the viewport width. This way, we move the rotation center of the element away from the user of a quantity equal to half the element width.
Its list items (portfolio projects) are then used to create the different faces of the parallelepiped and are translated/rotated according to the face they are on. For example, the front-face just needs to be translated back along the Z-axis, while the right face needs to be rotated along the Y-axis and translated back.
Here's a simple animation explaining this concept (created using Adobe After Effects):
.cd-3d-portfolio .projects .row {
height: 25vh;
position: relative;
z-index: 1;
/* position its children in a 3d space */
transform-style: preserve-3d;
transform: translateZ(-50vw);
transition: transform 0.6s cubic-bezier(0.5, 0, 0.1, 1);
}
.cd-3d-portfolio .projects .row > li {
/* this is the single project */
position: absolute;
z-index: 1;
height: 100%;
width: 100%;
overflow: hidden;
}
.cd-3d-portfolio .projects .row > li.front-face {
transform: translateZ(50vw);
}
.cd-3d-portfolio .projects .row > li.right-face {
transform: rotateY(90deg) translateZ(50vw);
}
.cd-3d-portfolio .projects .row > li.left-face {
transform: rotateY(-90deg) translateZ(50vw);
}
.cd-3d-portfolio .projects .row > li.back-face {
transform: rotateY(180deg) translateZ(50vw);
}
When the user selects one of the filters in the top navigation, each ul.row
is rotated to reveal the selected face (more in the Event handling section).
As for the single projects, the project preview image is set as background-image of the .project-image::before
element; it has an absolute position and a height of 240% the project height (which means, 60% of the viewport height); this way only a portion of the preview image is visible due to the overflow property of its .row > li
ancestor.
The project content is wrapped inside the .project-content
element which is placed right below the project preview image. When a project is open, the overflow property of the .row > li
element is changed to reveal its content.
.cd-3d-portfolio .projects .project-image {
position: relative;
width: 100%;
height: 25%;
transition: transform 0.6s;
}
.cd-3d-portfolio .projects .project-image::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 1;
height: 240%;
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
.cd-3d-portfolio .projects .project-content {
position: absolute;
/* place the content right below the project image */
top: 60%;
width: 100%;
background: white;
}
For the 3D rotation to work, the transform-style
property of the .row
elements is set to preserve-3d
so that its children are placed in a 3D space. If the browser does not support this property, we replace the 3D effect with a fade-in/fade-out effect (we use Modernizr to check browser support).
.no-preserve3d .cd-3d-portfolio .projects .row {
/* fallback for browsers that don't support the preser3d property */
transform: translateZ(0);
}
.no-preserve3d .cd-3d-portfolio .projects .row > li {
opacity: 0;
transform: translateX(0);
}
.no-preserve3d .cd-3d-portfolio .projects .row > li.front-face,
.no-preserve3d .cd-3d-portfolio .projects .row > li.right-face,
.no-preserve3d .cd-3d-portfolio .projects .row > li.left-face,
.no-preserve3d .cd-3d-portfolio .projects .row > li.back-face {
transform: translateX(0);
}
.no-preserve3d .cd-3d-portfolio .projects .row > li.selected {
opacity: 1;
}
Events handling
To implement this 3D portfolio, we created a Portfolio3D
object and used the bindEvents
function to attach event handlers to the proper elements.
function Portfolio3D( element ) {
//define a Portfolio3D object
this.element = element;
this.navigation = this.element.children('.cd-3d-portfolio-navigation');
this.rowsWrapper = this.element.children('.projects');
this.rows = this.rowsWrapper.children(\.row');
this.visibleFace = 'front';
this.visibleRowIndex = 0;
this.rotationValue = 0;
//animating variables
this.animating = false;
this.scrolling = false;
// bind portfolio events
this.bindEvents();
}
if( $('.cd-3d-portfolio').length > 0 ) {
var portfolios3D = [];
$('.cd-3d-portfolio').each(function(){
//create a Portfolio3D object for each .cd-3d-portfolio
portfolios3D.push(new Portfolio3D($(this)));
});
}
The visibleFace
property is used to store the parallelepiped face visible at the moment (if the ul.row
has not been rotated, the visible face is the front face while, if it has been rotated of 90deg, the visible face is the left one and so on).
When the user selects a filter in the top navigation, the showNewContent()
method is used to move the selected faces in the right position and to rotate the ul.row
elements.
Portfolio3D.prototype.bindEvents = function() {
var self = this;
this.navigation.on('click', 'a:not(.selected)', function(event){
//update visible projects when clicking on the filter
event.preventDefault();
if( !self.animating ) {
self.animating = true;
var index = $(this).parent('li').index();
//show new projects
self.showNewContent(index);
//update filter selected element
//..
}
});
//...
};
According to whether the selected filter precedes or follows the one already selected, the ul.row
are rotated clockwise ('rightToLeft') or anticlockwise ('leftToRight').
The getRotationPrameters
method uses this direction value plus the visibleFace
property value to determine the new rotation value of the ul.row
; additionally, it takes care of determining which face is going to be visible to give it the proper classes. For example, if the face visible at the moment is the front face and we need to rotate the parallelepiped 'rightToLeft', then the new visible face will be the right one.
Portfolio3D.prototype.showNewContent = function(index) {
var self = this,
direction = ( index > self.visibleRowIndex ) ? 'rightToLeft' : 'leftToRight',
rotationParams = this.getRotationPrameters( direction ),
newVisibleFace = rotationParams[0],
rotationY = rotationParams[1],
translateZ = $(window).width()/2;
//rotate the parallelepiped
this.setTransform(rotationY, translateZ);
//update .row > li classes
//...
//update Portfolio3D properties
//..
};