Friday, November 09, 2012

Vertical showcase slider with jQuery and CSS transitions

Vertical showcase slider with jQuery and CSS transitions

A tutorial on how to create a responsive vertical fullscreen slider that moves its sections in opposite directions. We'll be using jQuery, CSS transitions and media queries to make the layout adaptive.

In this tutorial we will create a very simplistic and responsive product slider for an online store or a portfolio. The idea is to have different sections in a fullscreen view: the image or preview, a navigation and the description. When navigating through the items, we will slide the preview section and the section with the description in opposite directions. The idea for this kind of "opposite" transition comes from the beautiful website of the National LGBT Museum which moves the left and right side in the same manner when navigating or scrolling the page.

The product images and information used in the demo are by IKEA.

The MarkUp

We will have a main container which will wrap the following elements: a header, a wrapper for the content or descriptions and a wrapper for the slides:

<section id="ps-container" class="ps-container">

    <div class="ps-header">
        <h1>Vertical Showcase Slider</h1>
    </div><!-- /ps-header -->
     
    <div class="ps-contentwrapper">
     
        <div class="ps-content">
            <h2>Bernhard</h2>
            <span class="ps-price">£100</span>
            <p>With restful springiness in the seat; prevents static sitting and provides enhanced seating comfort. Padded seat and back for enhanced seating comfort. Soft, hardwearing and easy care leather, which ages gracefully.</p>
            <a href="http://www.ikea.com/gb/en/catalog/products/80163804/#/60203882">Buy this item</a>
        </div>
         
        <div class="ps-content">
            <!-- description item 2 -->
        </div>
         
        <div class="ps-content">
            <!-- description item 3 -->
        </div>
         
        <div class="ps-content">
            <!-- description item 4 -->
        </div>
         
        <div class="ps-content">
            <!-- description item 5 -->
        </div>
         
    </div><!-- /ps-contentwrapper -->
     
    <div class="ps-slidewrapper">
     
        <div class="ps-slides">
            <div style="background-image:url(images/1.jpg);"></div>
            <div style="background-image:url(images/2.jpg);"></div>
            <div style="background-image:url(images/3.jpg);"></div>
            <div style="background-image:url(images/4.jpg);"></div>
            <div style="background-image:url(images/5.jpg);"></div>
        </div>
         
        <nav>
            <a href="#" class="ps-prev"></a>
            <a href="#" class="ps-next"></a>
        </nav>
         
    </div><!-- /ps-slidewrapper -->
     
</section><!-- /ps-container -->

The wrapper for the slides will contain the same amount of divisions like the content wrapper and each division will have the respective image as background image. We will also have a navigation that will contain a previous and next anchor. These anchors will also have a background image, but we'll set it dynamically.

The CSS

Let's first add a font that we've created with fontello.com. This font will only have one character and it's the little shopping cart for the "Buy this item" link:

@font-face {
  font-family: 'icon';
  src: url("font/icon.eot");
  src: 
    url("font/icon.eot?#iefix") format('embedded-opentype'),
    url("font/icon.woff") format('woff'), 
    url("font/icon.ttf") format('truetype'), 
    url("font/icon.svg#icon") format('svg');
  font-weight: normal;
  font-style: normal;
}

Our aim is to create a layout that is 100% width and height of the screen, so our container will be positioned absolutely and we'll set the overflow to hidden:

.ps-container {
    position: absolute;
    width: 100%; height: 100%;
    overflow: hidden;
    text-transform: uppercase;
    color: #555;
    background: #fff;
}

The width and height will be 100%. Note that we've set the height of the html to 100% as well (demo.css).
All the children of our main container will have a width of 50% and they'll be of absolute positioning:

.ps-container > div {
    position: absolute;
    width: 50%;
}

There will be a couple of elements that will share the absolute positioning:

.ps-container > div > div,
.ps-slidewrapper > nav,
.ps-slides > div {
    position: absolute;
}

The header will have a height of 150 pixel and we'll position it in the top left corner:

.ps-header {
    top: 0; left: 0;
    height: 150px;
    z-index: 1001;
    background: #fff;
}

The h1 will be styled as follows:

.ps-header h1 {
    color: #ccc;
    line-height: 150px;
    margin: 0; padding: 0 50px;
    font-weight: 200; font-size: 14px;
    letter-spacing: 10px;
}

The content wrapper will need the same top as the height of the header and we'll set the overflow to hidden:

.ps-contentwrapper {
    top: 150px; bottom: 0;
    overflow: hidden;
    z-index: 1000;
}

The inner divisions will occupy all the width and height of the parent and we'll give it a bit of padding:

.ps-content {
    background: #fff;
    width: 100%; height: 100%;
    padding: 50px;
}

Let's style the text elements. The headline and the paragraph will have some borders to suggest a chair:

.ps-content h2 {
    margin: 10px 0 30px; padding: 10px 15px;
    border-right: 1px solid #f2f2f2;
    border-bottom: 1px solid #f2f2f2;
    letter-spacing: 4px;
    text-align: right;
    font-weight: 700;
}

.ps-content p {
    line-height: 26px;
    font-weight: 400; font-size: 12px;
    letter-spacing: 2px;
    word-spacing: 10px;
    padding: 10px 15px;
    text-align: justify;
    border-left: 1px solid #f2f2f2;
    border-top: 1px solid #f2f2f2;
}

The price will be floating on the left side and we'll give it the style of a box:

.ps-content span.ps-price {
    float: left;
    margin: 10px;
    width: 140px; height: 140px;
    line-height: 140px;
    text-align: center;
    color: #fff;
    background: #f7cfc6;
    background: rgba(247,197,185,0.8);
    font-weight: 200; font-size: 55px;
}

Note that we set a HEX background color before setting a RGBA one. Older browsers that don't know what RGBA values are will ignore the second line and apply the first color.
The link will have a thick border and we'll make it green on hover if we are not on a touch device (we use Modernizr for that):

.ps-content a:last-child {
    font-weight: 700; font-size: 14px;
    color: #555;
    letter-spacing: 4px;
    float: right;
    border: 3px solid #555;
    padding: 3px;
    text-indent: 4px;
}

.no-touch .ps-content a:last-child:hover {
    color: #b2d79d;
    border-color: #b2d79d;
}

We'll add the little shopping cart icon by styling the pseudo-class :after:

.ps-content a:last-child:before {
    content: '\53';
    font-family: 'icon';
    font-style: normal;
    font-weight: normal;
    speak: none;
    padding-right: 5px;
}

The container for the slides and navigation will be placed on the right side and it will have a height of 100%:

.ps-slidewrapper {
    right: 0; top: 0;
    height: 100%;
    overflow: hidden;
}

The wrapper for the slides will be stretched from top: 0 to bottom: 200px. This will keep its height elastic:

.ps-slides {
    top: 0; bottom: 200px;
    width: 100%;
}

The inner divs, the ones that will contain the background image, will have a width and height of 100% and we'll give them a inset box-shadow that will serve as a subtle overlay over the main image preview. We don't really know how big this division will get so we'll give it an extremely big spread radius value:

.ps-slides > div {
    width: 100%; height: 100%;
    box-shadow: inset 0 0 0 9999px rgba(179,157,250,0.1);
}

The navigation will be positioned at the bottom of the slides container and we'll give it a fixed height of 200 pixel:

.ps-slidewrapper > nav {
    width: 100%;height: 200px;
    bottom: 0; right: 0;
    z-index: 1000;
}

The previous and next link elements will be floating and we'll also give them a inset box-shadow to create a subtle overlay effect. They will also have a transition for non-touch devices:

.ps-slidewrapper > nav > a {
    width: 50%; height: 100%;
    position: relative;
    float: left;
    outline: none;
    box-shadow: inset 0 0 0 9999px rgba(207,227,206,0.8);
}

.ps-slidewrapper > nav > a:first-child { box-shadow: inset 0 0 0 9999px rgba(233,217,141,0.8); }
.no-touch .ps-slidewrapper > nav > a { transition: box-shadow 0.4s ease-in-out; }
.no-touch .ps-slidewrapper > nav > a:hover { box-shadow: inset 0 0 0 9999px rgba(246,224,121,0.1); }
.no-touch .ps-slidewrapper > nav > a:first-child:hover { box-shadow: inset 0 0 0 9999px rgba(249,15,15,0.1); }

The navigation anchors will have a pseudo-element that will be styled to appear as an arrow. For that we will add a left and top border and rotate them accordingly:

.ps-slidewrapper > nav > a:after {
    content: '';
    position: absolute;
    width: 100px; height: 100px;
    top: 50%; left: 50%;
    margin: -20px 0 0 -50px;
    transform: rotate(45deg);
    border-left: 1px solid #fff;
    border-top: 1px solid #fff;
}

.ps-slidewrapper > nav > a:first-child:after {
    transform: rotate(-135deg);
    margin: -80px 0 0 -50px;
}

The main previews and the navigation links are the elements that have a background image. We'll set that image to stretch over the height of their container:

.ps-slides > div, .ps-slidewrapper > nav > a {
    background-color: #fff;
    background-position: center top;
    background-repeat: no-repeat;
    background-size: auto 100%;
}

The next class is used dynamically when we want to slide an element in or out:

.ps-move { transition: top 400ms ease-out; }

Last, but not least, we'll define a media query for smaller devices. We only want the media query to change the style if we have JavaScript enabled since we have a completely different layout for the case JavaScript is disabled.

We need to set the children of the main container to 100% width:

@media screen and (max-width: 860px) {
    .js .ps-container > div { width: 100%; }

The header will be a bit smaller:

.js .ps-header { height: 50px; }

.js .ps-header h1 {
    line-height: 50px;
    padding: 0px 20px;
    letter-spacing: 4px;
}

The wrapper for the preview slides will be positioned differently since we'll place the content under it:

.js .ps-slides { bottom: 320px; top: 50px; }

The navigation will be half its original height:

.js .ps-slidewrapper > nav { height: 100px; }

The content wrapper will have a height of 220px and we'll place it right above the navigation:

.js .ps-contentwrapper {
    top: auto; bottom: 100px;
    height: 220px;
}

Let's change the sizes of the typographic elements:

.js .ps-content { padding: 10px; }

.js .ps-content h2 {
    border-right: none;
    font-size: 18px;
    margin: 10px 0; padding-top: 0;
}

.js .ps-content span.ps-price {
    font-weight: 700; font-size: 18px;
    width: 50px; height: 50px;
    line-height: 50px;
    margin-bottom: 0;
}

We don't have so much space, so let's set a fixed height for the paragraph and make it scrollable:

.js .ps-content p {
    line-height: 20px;
    border: none;
    padding: 5px 10px;
    height: 80px;
    overflow-y: scroll;
}

The link will be a bit smaller and positioned to fit better into its context:

.js .ps-content a:last-child {
        font-size: 13px;
        margin: 10px 20px 0 0;
    }
}

The Javascript

We will start by caching some elements and define some variables:

var $container = $( '#ps-container' ),
    $contentwrapper = $container.children( 'div.ps-contentwrapper' ),
    // the items (description elements for the slides/products)
    $items = $contentwrapper.children( 'div.ps-content' ),
    itemsCount = $items.length,
    $slidewrapper = $container.children( 'div.ps-slidewrapper' ),
    // the slides (product images)
    $slidescontainer = $slidewrapper.find( 'div.ps-slides' ),
    $slides = $slidescontainer.children( 'div' ),
    // navigation arrows
    $navprev = $slidewrapper.find( 'nav > a.ps-prev' ),
    $navnext = $slidewrapper.find( 'nav > a.ps-next' ),
    // current index for items and slides
    current = 0,
    // checks if the transition is in progress
    isAnimating = false,
    // support for CSS transitions
    support = Modernizr.csstransitions// transition end event
    // transition end event
    // https://github.com/twitter/bootstrap/issues/2870
    transEndEventNames = {
        'WebkitTransition' : 'webkitTransitionEnd',
        'MozTransition' : 'transitionend',
        'OTransition' : 'oTransitionEnd',
        'msTransition' : 'MSTransitionEnd',
        'transition' : 'transitionend'
    };

When the init function is called, the first thing to do is to show the first item and corresponding slide/image. Also, we need to update the navigation arrows background image with the right ones, meaning that we will use the same background images as defined for the previews. Finally, the initEvents function is called.

init = function() {

    // show first item
    var $currentItem = $items.eq( current ),
        $currentSlide = $slides.eq( current ),
        initCSS = {
            top : 0,
            zIndex : 999
        };

    $currentItem.css( initCSS );
    $currentSlide.css( initCSS );
     
    // update nav images
    updateNavImages();

    // initialize some events
    initEvents();

},
updateNavImages = function() {

    // updates the background image for the navigation arrows
    var configPrev = ( current > 0 ) ? $slides.eq( current - 1 ).css( 'background-image' ) : $slides.eq( itemsCount - 1 ).css( 'background-image' ),
        configNext = ( current < itemsCount - 1 ) ? $slides.eq( current + 1 ).css( 'background-image' ) : $slides.eq( 0 ).css( 'background-image' );

    $navprev.css( 'background-image', configPrev );
    $navnext.css( 'background-image', configNext );

},
adjustLayout = function() {

    $container.css( 'height', $window.height() );

},

We need to initialize the click event for both navigation elements and the transitionend event for both, items/descriptions and slides.

initEvents = function() {

    $navprev.on( 'click', function( event ) {

        if( !isAnimating ) {
             
            slide( 'prev' );
         
        }
        return false;

    } );

    $navnext.on( 'click', function( event ) {

        if( !isAnimating ) {
             
            slide( 'next' );
         
        }
        return false;

    } );

    // transition end event
    $items.on( transEndEventName, removeTransition );
    $slides.on( transEndEventName, removeTransition );
     
},

The main function is, of course, the slide function. The idea is to position the next item/slide to be shown above or below the current one (depending on which navigation element we click). Then we move the current item/slide out of the wrapper and slide in the new ones. We also keep the navigation elements' background image updated.

slide = function( dir ) {

    isAnimating = true;

    var $currentItem = $items.eq( current ),
        $currentSlide = $slides.eq( current );

    // update current value
    if( dir === 'next' ) {

        ( current < itemsCount - 1 ) ? ++current : current = 0;

    }
    else if( dir === 'prev' ) {

        ( current > 0 ) ? --current : current = itemsCount - 1;

    }
        // new item that will be shown
    var $newItem = $items.eq( current ),
        // new slide that will be shown
        $newSlide = $slides.eq( current );

    // position the new item up or down the viewport depending on the direction
    $newItem.css( {
        top : ( dir === 'next' ) ? '-100%' : '100%',
        zIndex : 999
    } );
     
    $newSlide.css( {
        top : ( dir === 'next' ) ? '100%' : '-100%',
        zIndex : 999
    } );

    setTimeout( function() {

        // move the current item and slide to the top or bottom depending on the direction 
        $currentItem.addClass( 'ps-move' ).css( {
            top : ( dir === 'next' ) ? '100%' : '-100%',
            zIndex : 1
        } );

        $currentSlide.addClass( 'ps-move' ).css( {
            top : ( dir === 'next' ) ? '-100%' : '100%',
            zIndex : 1
        } );

        // move the new ones to the main viewport
        $newItem.addClass( 'ps-move' ).css( 'top', 0 );
        $newSlide.addClass( 'ps-move' ).css( 'top', 0 );

        // if no CSS transitions set the isAnimating flag to false
        if( !support ) {

            isAnimating = false;

        }

    }, 0 );

    // update nav images
    updateNavImages();

};