Horizontal Portfolio Layout with CSS3 Animations and jQuery

In this tutorial today we're going to create a horizontal portfolio layout with cool hover effects inspired by those on Guillaume Tomasi's personal website. The website is made in Flash, so I thought it would be nice to recreate the flash hover effect of the portfolio items using CSS3 animations and transitions, and some jQuery to replicate the image pan effect on hover.

I've also added a simple falling down effect on scroll, where the portfolio items fall down as soon as they enter the visible area of the viewport.

The artwork used in this demo is used with the permission of their owner Vlad Gerasimov. You can find the original images/wallpapers and more on his website VladStudio.com.

Please note that this demo will work only in browsers that support the CSS3 properties used.

For the sake of brevity in the example code, I am using the un-prefixed CSS properties, but you will find the prefixes in the downloadeable source code on Github.

So, let's get started!

Our list of items is literally a list of items each one with a class item. Each item contains a figure which in turn contains a .view container which wraps an image inside it, and a footer with two paragraph tags that contain the meta information for each image, and a small date tag with its own animation.

    <ul class="portfolio-items">
<li class="item">
<div class="view">
<img src="images/1.jpg" />
<p><span><a href="http://www.vladstudio.com/wallpaper/?thetwoandthebubbles">The Two and The Bubbles</a></span></p>
<p><span>By Vlad Gerasimov</span></p>
<div class="date">2005</div>
<!-- second item -->
<!-- third item -->
<!-- and so forth -->

Let's start with basic styles for the items before we get into the animations and hover effects.

              .portfolio-items {
height: 400px;
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
margin-bottom: 30px;
position: relative;
.portfolio-items > li {
display: inline-block;
/*aligning items by top baseline makes sure the baseline doesn't change once the hover
effect is fired and therefore the other items stay put*/

vertical-align: top;
.item {
width: 300px;
height: 202px;
margin: 150px 20px 0;
padding: 5px;
box-shadow: 0px 10px 10px -5px rgba(0,0,0,0.5);
background-color: white;
font-size: 14px;
/*initially all items are moved 300px up and faded out and rotated, they will fade
into view and back to position later via javascript*/

opacity: 0;
position: relative;
top: -300px;
transform: rotate(-135deg);
transition: all .3s ease, opacity 2s ease, top 1s ease;
/*even items will be 100px lower than their siblings*/
.item:nth-child(even) {
margin-top: 100px;

Now that all items have been styled and placed, we'll define the styles for the inner components of each item.

The figure will take up the full width of the parent. The image will get both a height and a width, and we'll apply a transition to the items so that they change smoothly on hover.

The figcaption with the metadata will be positioned absolutely, and will be invisible at first so it gets a 0 opacity value.

.view {
overflow: hidden;
width: 100%;
height: 190px;
position: relative;
.view img {
width: 300px;
height: 190px;
transition: width .3s ease;
position: absolute;

figcaption {
height: 60px;
width: 100%;
padding: 0;
position: absolute;
bottom: 0;
overflow: hidden;
opacity: 0;
figcaption p {
font: bold 12px/18px "Arial", sans-serif;
text-transform: uppercase;
padding: 0 10px;
margin: 5px 0;
background-color: #f0f0f0;
/*the text of the paragraph tags in the footer(figcaption) is initially hidden to the left*/
figcaption span {
left: -100%;
opacity: 0;
figcaption a{
color: #CC320F;

.date {
z-index: 1;
width: 50px;
height: 30px;
line-height: 30px;
color: #fff;
font-weight: bold;
text-align: center;
border-radius: 1px;
background-color: #CC320F;
position: absolute;
bottom: 30px;
left: 15px;
transition: transform 0.5s cubic-bezier(0.12, 1.6, 0.91, 0.92);

Now that we have all the items styled, we'll define what happens when each item is hovered.

              .item:hover {
height: 270px;
padding: 15px;
transform: translateY(-68px);
.item:hover .date {
transform: translate3d(0, 61px, 0);
.item:hover figcaption {
animation: show .25s ease-in .120s forwards;
.item:hover p:nth-of-type(1) span{
animation: slideOut .25s ease-out .15s forwards;
.item:hover p:nth-of-type(2) span{
animation: slideOut .2s ease-out .3s forwards;
.item:hover .view {
height: 170px;
.item:hover .view img {
top: -20px;
left: -20px;

When the item is hovered, it increases in height, its padding is increased, thus decreasing the view or "viewport" for each image, while the image keeps its original size. We'll later add a panning effect to the image which makes it possible to view the whole image despite the fact that its viewport got smaller, by changing its position as the mouse moves over it; this is why the image is moved 20px to the left and upwards when its field of view decreases. We'll manipulate these positions with Javascript later.

Also on hover, the date tag slides down, the footer is shown and the metadata slides in.

Here are the keyframes defined for the above animations:

              /*animation to show the metadata*/
@keyframes slideOut {
0% {
left: -100%;
opacity: 0;
95% {
left: 0;
opacity: 0.2;
100% {
opacity: 1;
left: 0;
/*animation to show the footer, which will simply up its opacity to 1*/
@keyframes show {
to {
opacity: 1;

When we defined the initial styles for the items, we defined their position and opacity so that they are not visible at first, but once they are within the viewport's visible area, they get a class (via Javascript) which makes them "fall down" into position. Here is the class added to the items on scroll:

              .falldown {
top: 0;
opacity: 1;
/*they are also initially rotated, and are rotated back to their normal position*/
transform: rotate(0);

For extra styling purposes, we're gonna style the scrollbar of the items' list. But bear in mind that these styles are supported only in Webkit browsers. You can, of course, use one of several javascript plugins available to provide cross-browser scrollbar styles if it's necessary to your overall design.

              ::-webkit-scrollbar {
width: 7px;
height: 7px;
cursor: pointer;
::-webkit-scrollbar-track {
background-color: #ddd;
border-radius: 10px;
::-webkit-scrollbar-thumb {
border-radius: 10px;
background-color: #C4290D;

That's all the styling we need and all animations needed for the hover effect. Now we'll start defining the panning effect with Javascript and handling the list scroll function.

We'll start by defining the scrolling function which will first check for the position of an item on the screen, and return true if the item is in the visible area of the viewport, but it only checks horizontally.

              //checks if element it is called on is visible (only checks horizontally)
(function($) {
var $window = $(window);

$.fn.isVisible = function(){
var $this = $(this),
Left = $this.offset().left,
visibleWidth = $window .width();

return Left < visibleWidth;

Now we're going to define the function what will call this function on the portfolio items to check for their visibility.

var list = $('.portfolio-items'),
showVisibleItems = function(){
list.children('.item:not(.falldown)').each(function(el, i){
var $this = $(this);

We'll want to call this function as soon as the page has loaded to check for visible items and add the .falldown class to all items that should be visible in the beginning. Then, we'll want to call this function whenever the list is scrolled as well.

//initially show all visible items before any scroll starts

//then on scroll check for visible items and show them

The last thing we're going to do is add the panning effect for the images on hover. What this function does is that it checks the position of the mouse cursor when it moves over each image, and moves the image along with the movement of the cursor. It measures the distance between the cursor and the image's view boundaries, and then divides that by the part of the image that's hidden beyond the borders of the view, thus making sure the image does not move any extra than it should. The function calculations should make it clearer:

              list.on('mousemove','img', function(ev){
var $this = $(this),
posX = ev.pageX,
posY = ev.pageY,
data = $this.data('cache');
//cache necessary variables
data = {};
data.marginTop = - parseInt($this.css('top')),
data.marginLeft = - parseInt($this.css('left')),
data.parent = $this.parent('.view'),
$this.data('cache', data);

var originX = data.parent.offset().left,
originY = data.parent.offset().top;

//move image
'left': -( posX - originX ) / data.marginLeft,
'top' : -( posY - originY ) / data.marginTop

One thing remaining is making sure the image returns to its initial position when the mouse leaves the item so that everything goes back to its initial state:

              list.on('mouseleave','.item', function(e){
'left': '0',
'top' : '0'

To finish up, we're going to add mouse wheel support using jQuery Mouse Wheel plugin by Brandon Aaron:

              //add mouse wheel support with the jquery.mousewheel plugin
list.mousewheel(function(event, delta) {

this.scrollLeft -= (delta * 60);



Aaand we're done! :) I hope you liked this simple hover effect and found it useful.

Thanks a lot Fabrice Weinberg for helping me optimize and organize my Javascript code. :)

Level up your accessibility knowledge with the Practical Accessibility course!

I created a self-paced, get-right-down-to-it online video course for web designers and developers who want to start creating more accessible Web user interfaces and digital products today.

The course is now open for enrollment!

Real. Simple. Syndication.

Get my latest content in your favorite RSS reader. (What is RSS?)

Follow me on X (formerly Twitter)