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;
}
});
Besides, we used jQuery to add/remove classes (eg .projects-visible
class when the user clicks the action button) and implement a basic slider navigation (next/prev buttons, keyboard and swipe navigation).