要素をスクロールに追従させる jQuery プラグイン

Apple Store のサイドバーのように、要素をウィンドウのスクロールに追従させる jQuery プラグイン、jQuery Floating Widget を作りました。説明が難しいので、まずはデモをご覧ください!

このアイデア自体は新しいものではなくて、たとえば以下の記事で詳しく紹介されています:

この方法で基本的にうまくいくんですが、フッターの高さがある程度あると、下までスクロールしたときに該当要素がフッター領域に食い込んでしまう場合があります。そこで、上記記事での実装を参考に、要素の「動ける範囲」を制限するための処理などを加えたものを考えてみました。

例として、フロートによる 2 カラムのレイアウトで、サイドバーをスクロールに追従させる場合をもとに解説します。マークアップと基本的なスタイルは以下のとおりです:

<div class="header"> . . . </div>

<div class="content-wrapper">

    <div class="content"> . . . </div>

    <div class="sidebar">
        <div class="floating-widget">
            <!-- この要素がスクロールについてきます -->
        </div>
    </div>

</div>

<div class="footer"> . . . </div>
.content-wrapper {
    position: relative;
    *zoom: 1;
}
.content-wrapper:after {
    display: block;
    clear: both;
    height: 0.01px;
    content: "";
}
.content {
    float: left;
    width: 512px;
}
.sidebar {
    float: right;
    width: 256px;
}
.floating-widget {
    margin: 50px 0;
}

該当要素の動ける範囲を制限する祖先要素 (div.content-wrapper) には、スクロールが下まで来たときに該当要素を絶対配置するための position: relative; と、高さを取得するための clearfix が必要です。

プラグイン本体とその呼び出し方は以下のとおりです:

// プラグイン
(function ($) {
    $.fn.floatingWidget = function () {
        return this.each(function () {
            var $this = $(this),
                $parent = $this.offsetParent(),
                $window = $(window),
                top = $this.offset().top - parseFloat($this.css('marginTop').replace(/auto/, 0)),
                bottom = $parent.offset().top + $parent.height() - $this.outerHeight(true),
                floatingClass = 'floating',
                pinnedBottomClass = 'pinned-bottom';
            if ($parent.height() > $this.outerHeight(true)) {
                $window.scroll(function () {
                    var y = $window.scrollTop();
                    if (y > top) {
                        $this.addClass(floatingClass);
                        if (y > bottom) {
                            $this.removeClass(floatingClass).addClass(pinnedBottomClass);
                        } else {
                            $this.removeClass(pinnedBottomClass);
                        }
                    } else {
                        $this.removeClass(floatingClass);
                    }
                });
            }
        });
    };
})(jQuery);
// 呼び出し
$(function () {
    $('.floating-widget').floatingWidget();
});

ウィンドウのスクロール位置を見て、要素がスクロールに追従している状態 (.floating) と祖先要素の下端にとどまっている状態 (.pinned-bottom) というそれぞれの状態に応じてマークアップのクラスを書き換えています。また最初に該当要素と祖先要素の高さを比較し、サイドバーの高さがコンテンツを超えるような場合はなにも起こりません。

ちなみに jQuery の offsetParent()position プロパティが static 以外である直近の祖先要素を返すメソッドです。また outerHeight() メソッドはパディングとボーダーを含む要素の高さを返しますが、引数に true を渡すとさらにマージンも含めた高さを返します。

最後に、.floating.pinned-bottom というクラスに対してスタイルを追加します:

.floating-widget.floating {
    position: fixed;
    top: 0;
}
.floating-widget.pinned-bottom {
    position: absolute;
    bottom: 0;
    _position: static;
}

スクロールのたびにスクリプトでスタイルを書き換える方法もありますが、それだと動きがカクカクしがちなので、JavaScript の仕事は位置によってマークアップのクラスを変更するだけにして、配置は CSS にまかせるというアプローチを採りました。

position: fixed; を解釈しない IE6 ではスクロールに追従しませんが、下まで来ると position: absolute; が適用されて要素が突然移動してしまうので、ハックで static を上書きしています。

ソースコードは以下からどうぞ。恥ずかしながら GitHub デビューしました!