We’ve been experimenting with some motion effects to build a simple portfolio template. The idea is to show a gallery of projects as a separate, secondary module, with the first block still partially visible - just one click away.

This is a UX pattern we’re used to in mobile apps: if you tap on an element and it slides out, but not entirely, you know you can tap on it to bring it back. The challenge here was to make this work on bigger devices too.

Here is a quick animation that shows the flow of the resource:

This resource was inspired by this dribbble shot by the talented Javi Pérez.

Creating the structure

The HTML structure is composed by 3 main blocks: a .cd-intro-block element, containing the action button to reveal the projects slider, an unordered list ( .cd-slider ), which is the projects gallery/slider, and a .cd-project-content element with the single project.

<div class="cd-intro-block"> <div class="content-wrapper"> <h1>Squeezebox Portfolio Template</h1> <a href="#0" class='cd-btn' data-action="load-projects">Show projects</a> </div> </div> <!-- .cd-intro-block --> <div class="cd-projects-wrapper"> <ul class="cd-slider"> <li class="current"> <a href="#0"> <img src="img/img.png" alt="project image"> <div class="project-info"> <h2>Project 1</h2> <p>Lorem ipsum dolor sit amet.</p> </div> </a> </li> <li> <!-- project preview here --> </li> <!-- other projects here --> </ul> <ul class="cd-slider-navigation cd-img-replace"> <li><a href="#0" class="prev inactive">Prev</a></li> <li><a href="#0" class="next">Next</a></li> </ul> <!-- .cd-slider-navigation --> </div> <!-- .cd-projects-wrapper --> <div class="cd-project-content"> <div> <h2>Project title here</h2> <em>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt, ullam.</em> <!-- other content here --> </div> <a href="#0" class="close cd-img-replace">Close</a> </div> <!-- .cd-project-content -->

Adding style

When user clicks the a[data-action="show-projects"] , the .projects-visible class is added to the .cd-intro-block and the .cd-projects-wrapper : this class triggers the intro section animation and the projects slider entrance.

The .cd-intro-block is translated (along the Y axis) by 90%, while the .cd-projects-wrapper visibility is set to visible.

.cd-intro-block { transition: transform 0.5s; transition-timing-function: cubic-bezier(0.67, 0.15, 0.83, 0.83); } .cd-intro-block.projects-visible { /* translate the .cd-intro-block element to reveal the projects slider */ transform: translateY(-90%); box-shadow: 0 4px 40px rgba(0, 0, 0, 0.6); cursor: pointer; } .cd-projects-wrapper { position: fixed; z-index: 1; top: 0; left: 0; width: 100%; height: 100%; visibility: hidden; transition: visibility 0s 0.5s; } .cd-projects-wrapper.projects-visible { visibility: visible; transition: visibility 0s 0s; }

For the project slides entrance animation, the .slides-in class is added to each project item (with a delay of 50ms); this class triggers a different animation according to the screen size.

On mobile devices, each list item has, by default, width: 100% and opacity: 0; when the .slides-in class is added, the cd-translate animation is applied:

.cd-slider li { opacity: 0; } .cd-slider li.slides-in { opacity: 1; animation: cd-translate 0.5s; } @keyframes cd-translate { 0% { opacity: 0; transform: translateY(100px); } 100% { opacity: 1; transform: translateY(0); } }

On desktop devices (viewport width more than 900px), each list item has, by default, width: 26%, translateX: 400% and a rotate: -10deg; when the .slides-in class is added, each project is moved back to its original position (translateX:0) and rotated to 0 degree:

@media only screen and (min-width: 900px) { .cd-slider li { position: relative; float: left; width: 26%; top: 50%; transform: translateX(400%) translateY(-50%) rotate(-10deg); transition: opacity 0s 0.3s, transform 0s 0.3s; } .cd-slider li.slides-in { /* reset style */ animation: none; transform: translateY(-50%); } }

For the projects slider (desktop version only), all list items are in relative position, have a fixed width (26%) and a float: left; the .cd-slider total width is properly changed (using javascript - more in the Events handling section) so that all list items are placed on the same row.

When a user clicks the .next / .prev button, the .cd-slider list element is translated (to the left/right respectively) by an amount equal tree times a single list item width + its left margin.

To create the 'squeezebox' animation, each list item is also animated using the cd-slide-n animation:

@keyframes cd-slide-n { 0%, 100% { transform: translateY(-50%); } 50% { transform: translateY(-50%) translateX(translateValue); } }

Basically, a different animation has been defined for each list item visible during the .cd-slider translation (a total of 6); each animation differs in the translateX value set in the 50% keyframe.

The following gif shows the movement of 3 visible elements when user clicks the .next button (the .cd-slider translation has been removed):

Events handling

On desktop devices, we change the .cd-slider width so that its <li> children stay on the same row; we used the setSliderContainer() function to set this width (and change it on resize); plus, on window resize, we update the .cd-slider translate value:

function setSliderContainer() { var mq = checkMQ(); //function to check mq value if ( mq == 'desktop' ) { var slides = projectsSlider.children('li'), // projectsSlider = $('.cd-slider') slideWidth = slides.eq(0).width(), marginLeft = Number(projectsSlider.children('li').eq(1).css('margin-left').replace('px', '')), sliderWidth = ( slideWidth + marginLeft )*( slides.length ) + 'px', slideCurrentIndex = projectsSlider.children('li.current').index(); //index of the first visible slide projectsSlider.css('width', sliderWidth); //if the first visible slide is not the first <li> child, update the projectsSlider translate value ( slideCurrentIndex != 0 ) && setTranslateValue(projectsSlider, ( slideCurrentIndex * (slideWidth + marginLeft) + 'px')); } else { //on mobile, reset style projectsSlider.css('width', ''); setTranslateValue(projectsSlider, 0); } resizing = false; } function setTranslateValue(item, translate) { item.css({ '-moz-transform': 'translateX(-' + translate + ')', '-webkit-transform': 'translateX(-' + translate + ')', '-ms-transform': 'translateX(-' + translate + ')', '-o-transform': 'translateX(-' + translate + ')', 'transform': 'translateX(-'\ + translate + ')', }); } var resizing = false; setSliderContainer(); $(window).on('resize', function(){ //on resize - update projectsSlider width and translate value if( !resizing ) { window.requestAnimationFrame(setSliderContainer); resizing = true; } });