Friday, December 14, 2012

Sticky captions concept

Sticky captions concept

When creating thumbnail grids, we usually want to show image captions on hover to provide more information about the item. Image captions are usually shown in a very specific part of the thumbnail, either on the top, the middle or the bottom. When adding captions to the bottom of a thumbnail it can happen that a thumbnail that is overflowing the viewport (i.e. is partly beyond the "fold") is being hovered but the caption won’t be seen because it appears on the bottom part of the image that is not visible. The user would have to scroll the page in order to see the bottom of the item and eventually the caption.

A small trick can solve that problem by simply making the caption "sticky". This would mean that the caption will be visible not only at the bottom of every thumbnail but also in any place, sticking at the bottom of the page, if the thumbnail hovered is overflowing the current view.

What we basically do is to imitate position: sticky but since it's not yet supported in many browsers, we'll do a bit of JavaScript to achieve the same result. We'll be using jQuery.

The main idea is to see when a hovered element overflows the viewport and show the caption in the right place by changing its position from absolute to fixed.

As an example, let’s take a simple grid with figures and figcaptions:

<div class="grid clearfix" id="grid">

    <figure>
        <img src="images/4.jpg">
        <figcaption>
            <a href="http://drbl.in/fWMT">Fall 7 Times Stand Up 8</a> by Erika Mackley
        </figcaption>
    </figure>

    <figure><!-- ... --></figure>
    <figure><!-- ... --></figure>
    <figure><!-- ... --></figure>
    <!-- ... -->
     
</div>

In our demo we use jQuery Masonry for creating a neat grid.

Let's take a look at the structural styling of the figcaption element. The caption should be absolute and we will set a left of auto. This is important because when we switch to fixed positioning, we want the caption to start at the left of its parent container. We set a bottom value of -60 pixels in order to hide the caption (minus its own height). Note that the parent container needs to have its overflow set to hidden.

We also add a little transition which will animate the bottom to 0 when we hover over the figure:

.grid figure figcaption {
    position: absolute;
    left: auto; bottom: -60px;
    width: 100%; height: 60px;
    transition: bottom 0.2s ease-in-out;
}

.grid figure:hover figcaption { bottom: 0; }

Now, we want to check if the thumbnail that is being hovered is cut off at the bottom, i.e. out of the window view. If yes, we will change the caption position to fixed and set a width to it (otherwise it would just happily expand until the end of the page).

Let's start by creating a custom jQuery selector called :bottomInViewport that we will help us determine if the caption of the image should be rendered or not.

$.extend( $.expr[':'], {
    bottomInViewport : function( el ) {
        var scrollTop = ( document.documentElement.scrollTop || document.body.scrollTop ),
            elOffsetTop = $( el ).offset().top,
            elH = $( el ).height(),
            descrH = $( el ).find( 'figcaption' ).outerHeight(true),
            winH = ( window.innerHeight && window.innerHeight < $( window ).height() ) ? window.innerHeight : $( window ).height();

        return ( elOffsetTop + elH > scrollTop && elOffsetTop + elH < scrollTop + winH ) || ( scrollTop + winH - elOffsetTop < descrH );
    }
});

The function returns true if the element is completely inside the viewport, meaning that the bottom part is not cut off. It also returns true if the element is not in the viewport but the visible part is just too small to fit the caption. In these cases our script shouldn't do anything and the caption will simply show as usual. If the function returns false, we will show the caption by setting its position to fixed. Since we defined the bottom to be 0 in the CSS, it will show at the bottom of the page, right where we need it. We'll also have to set the width to its parent's width (the figure):

function changeToFixed( $description, itemWidth ) {
    $description.css({ position: 'fixed', width: itemWidth });
}

function resetStyle( $description, delay ) {
    setTimeout( function() { $description.css({ position: 'absolute', width: '100%'}); }, delay || 0 );
}

We will need to bind the mouseenter and mouseleave events to the items and also the scroll event to the window while hovering over an item:

$items.on( 'mouseenter mouseleave', function( event ) {

    var $item = $( this ), itemWidth = $item.width(),
        $description = $item.find( 'figcaption' );

    switch( event.type ) {
        case 'mouseenter' :

            if( !$item.is( ':bottomInViewport' ) ) {
                $item.data( 'sticky', true );
                changeToFixed( $description, itemWidth );
            }
             
            $( window ).on( 'scroll', function () {
                var inviewport = $item.is( ':bottomInViewport' );
                if( !inviewport && !$item.data( 'sticky' ) ) {
                    $item.data( 'sticky', true );
                    changeToFixed( $description, itemWidth );
                }
                else if( inviewport && $item.data( 'sticky' ) ) {
                    $item.data( 'sticky', false );
                    resetStyle( $description );
                }
            } );

            break;
         
        case 'mouseleave' :

            if( $item.data( 'sticky' ) ) {
                $item.data( 'sticky', false );
                resetStyle( $( this ).find( 'figcaption' ), 200 );
            }
            $( window ).off( 'scroll' );
            break;
    }

} );

When we hover over an item, we check if we should apply our trick or not. Also, we bind the scroll event to the window, where we will check if the trick should be applied or reset as we scroll. When we leave the item, we unbind the scroll event from the window and we reset the item's style (if the trick was previously applied to it).

And that's it! Take a look at the demo and hover over an item that is cut off at the bottom to see the effect. And then scroll. You will see how the caption sticks at the bottom until its "natural" position is reached.

Credits: the demo features Dribbble shots of illustrations by Erika Mackley.