スクロール時にちらつかない IE6 向け position:fixed

更新履歴

2010-01-21
スクロール時のガタつきをはじめとする諸問題等を解決した精度の高い fixed を使いたい場合はこちらをご覧ください。

前回のつづきです。

スクロール時のちらつき

前回までのやり方は、スクロールイベント内で absolute 要素の表示位置をずらす事で position:fixed を実現してましたが、この方法だとスクロールの際、対象要素にちらつきが生じます。

今回はこのちらつきを解消する方法について考えてみます。

サンプルページ

(本エントリのサンプルページは IE6 でご覧ください。)

表示位置のずらしこみを行わない

スクロールイベントでの表示位置のずらしこみがちらつきの原因なので、別の方法で position:fixed させてみます。
THE HAM MEDIA さんのこちらの記事「CSSのpositionのまとめ」で紹介されてる方法を使うと簡単な CSS で position:fixed が実現できます。

html,body{   
    width :100%;
    height:100%;   
    overflow:auto;   
    margin:0;   
    padding:0;   
}   
div#samp{   
    position: fixed!important;   
    position: absolute;   
}

サンプルページ


absolute 要素は自身にとってのコンテナ要素(relative な先祖要素あるいは html要素)のスクロールに追従します。
この手法の場合 html 要素がそのコンテナにあたり、その html 要素を body 要素で覆い body 要素のスクロールバーのみを表示させることで、absolute 要素の位置固定を実現してます。

body 要素に margin をつけて試すと理解しやすいです。

サンプルページ

外側のスクロールバーを動かした時のみ absolute 要素が追従します。


お手軽で便利なのですが欠点もあります。

right , bottom ベース指定の absolute 要素がスクロールバーに重なって表示される

html コンテナを基点に位置が決まるため(16px以下の値の) right , bottom ベースの指定で absolute 要素を表示させると、対象要素がスクロールバーの上に重なって表示されてしまいます。

サンプルページ

このような場合は overflow:scroll などで、スクロールバーを常時表示させた上でスクロールバーの幅分 absolute 要素の表示位置をずらす等の工夫が必要になるかと思います。

html 要素、body 要素を個別にデザインできない

前述の通り body 要素が html 要素を覆うことになるので、html 要素 と body 要素を分けた(個別に背景色を設定するなどの)デザインができません。

全ての absolute , relative 要素が固定表示されてしまう

これが一番きついかと思います。relative 要素が固定されるのは IE6 のバグのようです。

サンプルページ

jQuery で解決できないか考えてみる

jQuery を使ってこれらの問題を解決できないか考えて見ます。

「right , bottom ベース指定の absolute 要素がスクロールバーに重なる」件

スクロールバーの表示の有無を判定し、表示されてる場合のみスクロールバーの幅分表示位置をずらすことで対応できないでしょうか。
まず、スクロールバー表示有無判定ロジックです。

var isDisplayScrollBar=function(target,key){
    var val=target.css('overflow-'+key)
    if(val=='scroll')return true;
    if(val=='hidden')return false;
//  var ret=false,pos,method='scroll'+(key=='y'?'Top':'Left');
//  if(pos=target[method]()>0)return true
//  target[method](1)
//  if(target[method]()==1)ret=true
//  target[method](pos)
//  return ret;
    if(val=='auto'){
        var el = target.get(0), method=(key=='y'?'Height':'Width');
        return el['client'+method] < el['scroll'+method];
    }
    return false
}
var scrollBarWidth=function(target){
    return {
        x : isDisplayScrollBar(target,'x')?16:0,
        y : isDisplayScrollBar(target,'y')?16:0
    }
}

サンプルページ

scroll プロパティの値で表示の有無をおおよそ判定できますが、判断が難しいのは scroll:auto で表示されていて、スクロール量が 0 の場合です。
この場合、$j('body').scrollTop(1) などとした後、スクロール位置が 1 になっているか否かで表示の有無を判定してます。乱暴なやり方な気もしますが、良い方法が思い浮かばなかったのでこんな感じにしてしまってます。

2008/12/7追記
codeなにがしで質問させてもらったところ、scrollHeightとclientHeightの比較で簡潔に書けることを教えていただきました!
javascript(jQuery)を使ってスクロールバーが表示されてるか否か判断する方法 - codeなにがし

サンプルページ


次に位置調整です。

var fixed=[]
$j('div.fixed').each(function(){
    if($j(this).css('top')=='auto'||$j(this).css('left')=='auto'){
        var $e=$j(this);
        fixed.push($e.data('pos',{
            'top':$e.css('top'),
            'right':$e.css('right'),
            'bottom':$e.css('bottom'),
            'left':$e.css('left')
        }));
    }
})
fixed=$j(fixed)
var adjust = function(){
    var offset=scrollBarWidth($j('body'))
    fixed.each(function(){
        var pos=this.data('pos')
        this.css(pos)
        if(offset.y>0 && pos.left=='auto')this.css('right',parseInt(pos.right)+offset.y)
        if(offset.x>0 && pos.top=='auto')this.css('bottom',parseInt(pos.bottom)+offset.x)
    })
}
adjust()
var tm=0;
$j(window).resize(function(){
    if(!tm){
        tm=setTimeout(function(){
            adjust()
            tm=0;
        },10)
    }
})

サンプルページ(IE6でご覧ください)



初期の位置情報を保持しておき、ブラウザのリサイズイベントで初期位置にリセット後、スクロールバーが表示されてる時のみ位置をずらします。

位置ずれの件はこれで解決しました。


「全ての absolute , relative 要素が固定表示されてしまう」件

body要素の直下に擬似的な html 要素と body 要素を配置し、この擬似要素で html,body 要素を覆います。
position:fixed させたい要素は、擬似 html 要素の弟として配置し、その他の要素は 擬似 body 要素内に配置させる事で、スクロールに対する追従をコントロールします。

「html 要素、body 要素を個別にデザインできない」件

それぞれの要素の主要プロパティを擬似要素にコピーして、デザインを復元します。


とりあえず普通にマークアップしてみます。

サンプルページ


次に以下処置(ソースの修正)をします。

    • body 要素直下に html , body の擬似要素となる div を配置
    • html , body 要素の主要プロパティを div 要素にコピー
    • 擬似要素で html , body 要素を覆う
    • fixed 要素のみ 擬似 html 要素の弟として配置する
    • fixed 要素がスクロールバーに重なる件の処置をする

サンプルページ(IE6でご覧ください)


処置前のデザインが再現され、fixed 要素の固定表示と absolute,relative 要素のスクロールの追従が確認できます。
上記の処置を jQuery で書いてみます。

var isDisplayScrollBar=function(target,key){
    var val=target.css('overflow-'+key)
    if(val=='scroll')return true;
    if(val=='hidden')return false;
    if(val=='auto'){
        var el = target.get(0), method=(key=='y'?'Height':'Width');
        return el['client'+method] < el['scroll'+method];
    }
    return false
}
var scrollBarWidth=function(target){
    return {
        x : isDisplayScrollBar(target,'x')?16:0,
        y : isDisplayScrollBar(target,'y')?16:0
    }
}

var fixed=[]
$j('div.fixed').each(function(){
    var $e=$j(this);
    fixed.push($e.data('pos',{
        'top':$e.css('top'),
        'right':$e.css('right'),
        'bottom':$e.css('bottom'),
        'left':$e.css('left')
    }));
})
fixed=$j(fixed)
var adjust = function(){
    var offset=scrollBarWidth($j('div.html'))
    fixed.each(function(){
        var pos=this.data('pos')
        this.css(pos)
        if(offset.y>0 && pos.left=='auto')this.css('right',parseInt(pos.right)+offset.y)
        if(offset.x>0 && pos.top=='auto')this.css('bottom',parseInt(pos.bottom)+offset.x)
    })
}
adjust()
var tm=0;
$j(window).resize(function(){
    if(!tm){
        tm=setTimeout(function(){
            adjust()
            tm=0;
        },10)
    }
})

var baseCssProp = [
    'margin-top',
    'margin-right',
    'margin-bottom',
    'margin-left',
    'padding-top',
    'padding-right',
    'padding-bottom',
    'padding-left',
    'background-color',
    'background-image',
    'width',
    'height',
    'overflow-x',
    'overflow-y'
]
var bodyCssProp = [
    'border-top-width',
    'border-top-style',
    'border-top-color',
    'border-right-width',
    'border-right-style',
    'border-right-color',
    'border-bottom-width',
    'border-bottom-style',
    'border-bottom-color',
    'border-left-width',
    'border-left-style',
    'border-left-color'
]
var getCSS=function(target,names){
    var ret={},name
    for(var i=0;i<names.length;i++){
        var name=names[i]
        ret[name]=target.css(name)
    }
    return ret;
}
var html=$j('html'),body=$j('body').wrapInner('<div class="html"><div class="body"></div></div>')
var vhtml=$j('div.html'),vbody=$j('div.body')
fixed.appendTo('body')

vbody.css(getCSS(body,baseCssProp))
vbody.css(getCSS(body,bodyCssProp))
vhtml.css(getCSS(html,baseCssProp))

$j('html,body').css({
    overflow:'hidden',
    width:'100%',
    height:'100%',
    margin:0,
    border:'none'
})
vhtml.css({
    position:'relative',
    width:'100%',
    height:'100%',
    margin:0,
    border:'none'
})

サンプルページ(IE6でご覧ください)

先ほどのサンプルと同様の動きをします。

プラグイン

プラグイン化してみます。
マークアップは普通に行い、以下のようにプラグインを実行するのみでちらつきの無い position:fixed が実現できます。

jQuery(function($j){
    $j('div.fixed').layerFixed();
})

IE6 以外のブラウザの場合は、純正の position:fixed が適用されます。

サンプルページ

ソース
$j.isDisplayScrollBar=function(target,key){
    var val=target.css('overflow-'+key)
    if(val=='scroll')return true;
    if(val=='hidden')return false;
    if(val=='auto'){
        var el = target.get(0), method=(key=='y'?'Height':'Width');
        return el['client'+method]<el['scroll'+method];
    }
    return false
}
$j.fn.isDisplayScrollBar=function(key){
    return $j.isDisplayScrollBar(this,key)
}
$j.scrollBarWidth=function(target){
    return {
        x : target.isDisplayScrollBar('x')?16:0,
        y : target.isDisplayScrollBar('y')?16:0
    }
}
$j.fn.scrollBarWidth=function(){
    return $j.scrollBarWidth(this)
}

$j.adjustFixed = function(target){
    return target.each(function(idx){
        new adjustFixed(target.eq(idx))
    })
}
$j.fn.adjustFixed = function(){
    return $j.adjustFixed(this)
}
var adjustFixed = function(target){
    var o=this;
    o.sts={
        target:target,
        container:o.getContainer(target)
    }
    o.setFixed();
    o.adjust()  
    var tm=0;
    $j(window).resize(function(){
        if(!tm){
            tm=setTimeout(function(){
                o.adjust()
                tm=0;
            },10)
        }
    })
}
$j.extend(adjustFixed.prototype,{
    getContainer : function(target){
        var relative = target.parents().filter(function(){
            var pos=$j(this).css('position');
            if(pos=='relative' || pos=='absolute')return this;
        })
        if(relative.size()==0)return $j(window)
        return relative.eq(0)
    },
    setFixed : function(){
        var o=this;
        var fixed=[]
        o.sts.target.each(function(){
            var $e=$j(this);
            fixed.push($e.data('pos',{
                'top':$e.css('top'),
                'right':$e.css('right'),
                'bottom':$e.css('bottom'),
                'left':$e.css('left')
            }));
        })
        this.fixed=$j(fixed)
    },
    adjust : function(){
        var o=this;
        var offset=o.sts.container.scrollBarWidth()
        this.fixed.each(function(){
            var pos=this.data('pos')
            this.css(pos)
            if(offset.y>0 && pos.left=='auto')this.css('right',parseInt(pos.right)+offset.y)
            if(offset.x>0 && pos.top=='auto')this.css('bottom',parseInt(pos.bottom)+offset.x)
        })
    }
})
$j.layerFixed= function(target){
    return target.each(function(idx){
        new layerFixed(target.eq(idx))
    })
}
$j.fn.layerFixed= function(){
    return $j.layerFixed(this)
}
var layerFixed= function(target){
    var o=this;
    if(!o.ie6){
        return target.css('position','fixed');
    }
    target.css('position','absolute');
    if($j('div.html').size()==0){
        var html=$j('html'),body=$j('body').wrapInner('<div class="html"><div class="body"></div></div>')
        var vhtml=$j('div.html'),vbody=$j('div.body')
        vbody.css(o.getCSS(body,o.baseCssProp))
        vbody.css(o.getCSS(body,o.bodyCssProp))
        vhtml.css(o.getCSS(html,o.baseCssProp))
        $j('html,body').css({
            overflow:'hidden',
            width:'100%',
            height:'100%',
            margin:0
        })
        body.css({
            border:'none'
        })
        vhtml.css({
            position:'relative',
            width:'100%',
            height:'100%',
            margin:0,
            border:'none'
        })
    }
    target.adjustFixed().appendTo('body')
}
$j.extend(layerFixed.prototype,{
    ie6 : $.browser.msie && $.browser.version < 7.0,
    baseCssProp : [
        'margin-top',
        'margin-right',
        'margin-bottom',
        'margin-left',
        'padding-top',
        'padding-right',
        'padding-bottom',
        'padding-left',
        'background-color',
        'background-image',
        'width',
        'height',
        'overflow-x',
        'overflow-y'
    ],
    bodyCssProp : [
        'border-top-width',
        'border-top-style',
        'border-top-color',
        'border-right-width',
        'border-right-style',
        'border-right-color',
        'border-bottom-width',
        'border-bottom-style',
        'border-bottom-color',
        'border-left-width',
        'border-left-style',
        'border-left-color'
    ],
    getCSS : function(target,names){
        var ret={},name
        for(var i=0;i<names.length;i++){
            var name=names[i]
            ret[name]=target.css(name)
        }
        return ret;
    }
})
注意点

body 要素の幅の単位を % にする場合は、以下のように jQuerycss メソッドもしくは width メソッドで指定するようにしてください。

jQuery(function($j){
    $j('body').css('width','80%').find('div.fixed').layerFixed();
})

理由は(前回のエントリでも似たような話がありましたが)、cssメソッドで幅を取得しようとすると具体的な値がとれてしまうため、ブラウザリサイズ時の幅調整が正しく行われないためです。(以下参照)

<style>
    body{
        width:80%
    }
</style>
<script>
    jQuery(function($j){
        alert($j('body').css('width')) //800px
        $j('body').css('width','80%')
        alert($j('body').css('width')) //80%
    })
</script>