3D Portfolio Template

3D Portfolio Template

A portfolio template, with a filter that triggers the rotation of 3D sections.

Nucleo icons

Sponsored by Nucleo, a free application to collect, customize and export all your icons as icon font and SVG symbols. Made by the CodyHouse folks!

All the resources available on CodyHouse are released under the BSD-3-Clause license. You can support our project with a Paypal donation 🙌

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):

parallelepiped-animation

.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
   //..
};

Join our newsletter

Get our monthly recap with the latest CodyHouse news