Tuesday, October 16, 2012

Lateral on-scroll sliding with jQuery

Lateral on-scroll sliding with jQuery

After getting the request, we are going to show you how to create a "slide-in on scroll" effect. You've probably seen this cool effect on some websites, like on Nizo or in the portfolio section of brilliantly designed La Moulade. The main idea is to laterally slide in elements depending on the scroll position of the document. Dividing the page into a left and right side, we want to move the elements from "outside" of the page to the center when they are in the viewport. We will also add the option to move the elements in 3D space.

The theme of today's tutorial is going to be a timeline where we will have some circular elements on one side and the descriptions on the other. We'll be alternating the sides to create a random look.

So, let's start!

The Markup

The HTML structure is going to consist of a main container and some rows that we'll use to place the left and the right elements. The left and right elements will either contain a h2 heading, a circular link with a background image or a description in form of a h3 element with a link and a span:

<div id="ss-container" class="ss-container">
    <div class="ss-row">
        <div class="ss-left">
            <h2 id="november">November</h2>
        <div class="ss-right">
    <div class="ss-row ss-medium">
        <div class="ss-left">
            <a href="#" class="ss-circle ss-circle-1">Some title</a>
        <div class="ss-right">
                <span>November 28, 2011</span>
                <a href="#">Some Title</a>
    <!-- more rows... -->

For the circles we'll have three different sizes and we'll indicate that but giving the respective row the class of ss-small, ss-medium or ss-large.
Let's look at the style.


The container will occupy all the width and we'll set the overflow to hidden because we don’t want a scrollbar to appear when we move the left and right elements out of if:

    width: 100%;
    position: relative;
    text-align: left;
    float: left;
    overflow: hidden;
    padding-bottom: 500px;

To create the middle line throughout the container, we'll use a pseudo element that we’ll position in the middle of the container:

    position: absolute;
    width: 4px; height: 100%;
    background: rgba(17,17,22,0.8);
    top: 0; left: 50%;
    margin-left: -2px;
    content: '';

The row will serve as a wrapper for the left and right elements:

    width: 100%;
    clear: both;
    float: left;
    position: relative;
    padding: 30px 0;

The two lateral elements will occupy half of the width:

.ss-left, .ss-right{
    float: left;
    width: 48%;
    position: relative;

.ss-right{ padding-left: 2%; }
    text-align: right;
    float: left;
    padding-right: 2%;

The headings will have the following style:

.ss-container h2{
    font-size: 40px;
    text-transform: uppercase;
    text-shadow: 0px 1px 1px #fff;
    color: rgba(78,84,123,0.2);
    padding: 20px 0px;

.ss-container h3{
    margin-top: 34px;
    padding: 10px 15px;
    background: rgba(26, 27, 33, 0.6);
    text-shadow: 1px 1px 1px rgba(26, 27, 33, 0.8)

To create a circle, we'll set the border radius of the anchor to 50% and we’ll add some neat box-shadow:

    border-radius: 50%;
    overflow: hidden;
    display: block;
    text-indent: -9000px;
    text-align: left;
        0px 2px 5px rgba(0,0,0,0.7) inset, 
        0px 0px 0px 12px rgba(61,64,85,0.3);
    background-size: cover;
    background-color: #f0f0f0;
    background-repeat: no-repeat;
    background-position: center center;

We'll have three different circle sizes and depending on which side we are we'll make the circle float either left or right:

.ss-small .ss-circle{ width: 100px; height: 100px; }
.ss-medium .ss-circle{ width: 200px; height: 200px; }
.ss-large .ss-circle{ width: 300px; height: 300px; }
.ss-left .ss-circle{
    float: right;
    margin-right: 30%;

.ss-right .ss-circle{
    float: left;
    margin-left: 30%;

We'll use the pseudo element :before and :after in order to create the line and the arrow that will point to the middle line. The width will be defined as a percentage so that it adjust to the screen size. We'll also center it by setting the top to 50% and correct the position by setting the margin-top to -3px. Depending on where we are (left or right side) we want the position to be different:

    width: 29%; height: 0;
    border-bottom: 5px dotted #ddd;
    border-bottom: 5px dotted rgba(17, 17, 22, 0.3);
    box-shadow: 0px 1px 1px #fff;
    position: absolute;
    top: 50%;
    content: '';
    margin-top: -3px;

.ss-left .ss-circle-deco:before{ right: 2%;    }
.ss-right .ss-circle-deco:before{ left: 2%;    }

The little arrow will be created by the border style and depending on if it's a child of the left or right side, we'll set the according border and position:

    width: 0; height: 0;
    border-top: 10px solid transparent;
    border-bottom: 10px solid transparent;
    content: '';
    position: absolute;
    top: 50%;
    margin-top: -10px;

.ss-left .ss-circle-deco:after{
    right: 0;
    border-right: 10px solid rgba(17,17,22,0.8);

.ss-right .ss-circle-deco:after{
    left: 0;
    border-left: 10px solid rgba(17,17,22,0.8);

Because of the different circle sizes, we'll need to adjust the position of the headings on the other side. We want them to be at the height of the arrow, so we'll set the margins differently (the one for ss-small is already set in the circle itself):

.ss-container .ss-medium h3{ margin-top: 82px; }
.ss-container .ss-large h3{ margin-top: 133px; }
.ss-container .ss-left h3{ border-right: 5px solid rgba(164,166,181,0.8); }
.ss-container .ss-right h3{ border-left: 5px solid rgba(164,166,181,0.8); }

The style for the description:

.ss-container h3 span{
    color: rgba(255,255,255,0.8);
    font-size: 13px;
    display: block;
    padding-bottom: 5px;

.ss-container h3 a{
    font-size: 28px;
    color: rgba(255,255,255,0.9);
    display: block;

.ss-container h3 a:hover{ color: rgba(255,255,255,1); }

Each circle is going to have a different background-image:

.ss-circle-1{ background-image:url(../images/1.jpg); }
.ss-circle-2{ background-image: url(../images/2.jpg); }
.ss-circle-3{ background-image: url(../images/3.jpg); }
/* and so on... */

The Javascript

The main idea is to initially show all the side elements that are visible on the viewport. All the other elements will be hidden by setting their left or right value to -50%. If the perspective option is set to true, then these elements will be translated and rotated on the y-axis, with the opacity set to 0. On scroll, we want to show the lateral elements by sliding them to position 0 (left or right, respectively), or in case of perspective, translate / rotate them accordingly once they are in the viewport.

Let's start by defining some variables:

// the row elements
var $rows           = $('#ss-container > div.ss-row'),
    // we will cache the inviewport 
    // rows and the outside viewport rows
    $rowsViewport, $rowsOutViewport,
    // navigation menu links
    $links          = $('#ss-links > a'),
    // the window element
    $win            = $(window),
    // we will store the window sizes here
    winSize         = {},
    // used in the scroll setTimeout function
    anim            = false,
    // page scroll speed
    scollPageSpeed  = 2000 ,
    // page scroll easing
    scollPageEasing = 'easeInOutExpo',
    // perspective?
    hasPerspective  = true,
    perspective = hasPerspective && Modernizr.csstransforms3d,

We'll have the following functions:

// initialize function
init            = function() {      
    // get window sizes
    // initialize events
    // define the inviewport selector
    // gets the elements that match the previous selector
    // if perspective add css
    if( perspective ) {
            '-webkit-perspective'       : 600,
            '-webkit-perspective-origin'    : '50% 0%'
    // show the pointers for the inviewport rows
    // set positions for each row

defineViewport defines a selector that gathers the row elements that are initially visible. An element is visible if its top is less than the window’s height. These elements will not be affected when scrolling the page:

defineViewport  = function() {
    $.extend( $.expr[':'], {
        inviewport  : function ( el ) {
            if ( $(el).offset().top < winSize.height ) {
                return true;
            return false;

setViewportRows checks which rows are initially visible:

setViewportRows = function() {
    $rowsViewport       = $rows.filter(':inviewport');
    $rowsOutViewport    = $rows.not( $rowsViewport )

getWinSize gets the window width and height:

getWinSize      = function() {
    winSize.width   = $win.width();
    winSize.height  = $win.height();

And now let's initialize some events:

initEvents      = function() {
    // navigation menu links.
    // scroll to the respective section.
    $links.on( 'click.Scrolling', function( event ) {
        // scroll to the element that has id = menu's href
        $('html, body').stop().animate({
            scrollTop: $( $(this).attr('href') ).offset().top
        }, scollPageSpeed, scollPageEasing );
        return false;
        // on window resize we need to redefine 
        // which rows are initially visible 
        // (this ones we will not animate).
        'resize.Scrolling' : function( event ) {
            // get the window sizes again
            // redefine which rows are initially 
            // visible (:inviewport)
            // remove pointers for every row
            // show inviewport rows and respective pointers
            $rowsViewport.each( function() {
                       .css({ left   : '0%' })
                       .css({ right  : '0%' })
        // when scrolling the page change 
        // the position of each row 
        'scroll.Scrolling' : function( event ) {
            // set a timeout to avoid that the 
            // placeRows function gets called on 
            // every scroll trigger
            if( anim ) return false;
            anim = true;
            setTimeout( function() {
                anim = false;
            }, 10 );


placeRows sets the position of the rows (left and right row elements). Both of these elements will start with -50% for the left/right (not visible). This value should be 0% (final position) when the element is in the center of the window.

placeRows       = function() {      
        // how much we scrolled so far
    var winscroll   = $win.scrollTop(),
        // the y value for the center of the screen
    winCenter   = winSize.height / 2 + winscroll;
    // for every row that is not inviewport
    $rowsOutViewport.each( function(i) {
        var $row    = $(this),
            // the left side element
            $rowL   = $row.find('div.ss-left'),
            // the right side element
            $rowR   = $row.find('div.ss-right'),
            // top value
            rowT    = $row.offset().top;
        // hide the row if it is under the viewport
        if( rowT > winSize.height + winscroll ) {

            if( perspective ) {

                    '-webkit-transform' : 'translate3d(-75%, 0, 0) rotateY(-90deg) translate3d(-75%, 0, 0)',
                    'opacity'           : 0
                    '-webkit-transform' : 'translate3d(75%, 0, 0) rotateY(90deg) translate3d(75%, 0, 0)',
                    'opacity'           : 0

            else {

                $rowL.css({ left        : '-50%' });
                $rowR.css({ right       : '-50%' });


        // if not, the row should become visible 
        // (0% of left/right) as it gets closer to 
        // the center of the screen.
        else {
                // row's height
            var rowH    = $row.height(),
                // the value on each scrolling step 
                // will be proporcional to the distance 
                // from the center of the screen to its height
                factor  = ( ( ( rowT + rowH / 2 ) - winCenter ) / ( winSize.height / 2 + rowH / 2 ) ),
                // value for the left / right of each side of the row.
                // 0% is the limit
                val     = Math.max( factor * 50, 0 );
            if( val <= 0 ) {
                // when 0% is reached show the pointer for that row
                if( !$row.data('pointer') ) {
                    $row.data( 'pointer', true );
            else {
                // the pointer should not be shown
                if( $row.data('pointer') ) {
                    $row.data( 'pointer', false );
            // set calculated values
            if( perspective ) {

                var t = Math.max( factor * 75, 0 ),
                    r = Math.max( factor * 90, 0 ),
                    o = Math.min( Math.abs( factor - 1 ), 1 );

                    '-webkit-transform' : 'translate3d(-' + t + '%, 0, 0) rotateY(-' + r + 'deg) translate3d(-' + t + '%, 0, 0)',
                    'opacity'           : o
                    '-webkit-transform' : 'translate3d(' + t + '%, 0, 0) rotateY(' + r + 'deg) translate3d(' + t + '%, 0, 0)',
                    'opacity'       : o

            else {

                $rowL.css({ left    : - val + '%' });
                $rowR.css({ right   : - val + '%' });


return { init : init };

View Demo#1    View Demo#2