width、heightがautoの要素にtransitionを適用する
CSS3のtransition。
一般的なウェブアプリのUIで動きが必要になる時って、始点と終点がある程度決まっていて、animation
の@keyframe
を駆使するような動きは限定的なことが多いと思うのだけど。
このtransitionを使っていていつも悩ましいのが、width
やheight
がauto
に設定されている要素にtransition
を適用したい時です。
transition
を有効にする場合、始点と終点の値を100px
のように絶対値で指定するか、50%
(もちろんheightなどで親のautoを引継いでしまっている場合はダメ)のように相対値で指定することはできるけど、auto
だとtransition
が効かない。
// Stylus .foo width 200px height 0 transition height .2s &.opened height auto
上の例の場合、
// JavaScript document.querySelector('.foo').classList.add('opened');
のようにclass="opened"
になるとheight
の値は0
からauto
に変わるが、transition
による効果は適用されない。
同様に、
// JavaScript document.querySelector('.foo').classList.remove('opened');
のようにopened
を取り去ると、height
の値はauto
から0
に変わるが、この場合もtransition
の効果は発生しない。
ドロップダウンメニューや開閉可能なペインのように、コンテンツの内容に応じてリサイズしたい場合に困る。
これを解決する良い方法はまだよく分からないのですが、実際に試してみた幾つかのアプローチをメモしておきます。
基本方針
JavaScriptによる操作は最低限にしたい。
class
の付け外しなどにとどめ、インラインスタイルの値を直接いじるのは最終手段にしたい。
auto
を使わない
いきなり解決して無いですが、サイズを明示的に指定できる場合は極力指定する、という方法。
例えば、(折り返しを許可しない)一行だけのインラインが含まれるような要素の場合line-height
+padding-top
+padding-bottom
でheight
を指定できる。
インラインが取り得る文字数の範囲が予測できる場合は、width
の値も明示できる可能性がある。
// Stylus .foo line-height 16px padding 6px 0 width 200px height 0 transition height .2s &.opened height @line-height+@padding-top*2
20px
のような絶対値の指定でなくても、80vh
、calc(50% + 20px)
のような指定であればtransition
は効くので、それらで解決できないかも検討してみる。
max-height
、max-width
を使用する
height
、width
の代わりにmax-height
、max-width
を使用する方法。
transition-property
にはmax-height
、max-width
を指定する。
height
の場合
以下のようにmax-height
を100vh
のように指定しておくと、コンテンツの高さに合わせて.foo
の高さが決まる(ただしコンテンツが100vh
以上の場合は100vh
になる)。
// Stylus .foo width 200px max-height 0 transition max-height .2s &.opened max-height 100vh
width
の場合
display: block
の場合、width
は親要素いっぱいに広がろうとするので、コンテンツのサイズに合わせたい場合はdisplay: inline-block
かdisplay: flex
にする必要がある。
// Stylus .foo display inline-block max-width 0 height 500px transition max-width .2s &.opened max-width 100%
ただし、以下のようなデメリットがある為、きめ細く動きを調整したい場合には向いていない。
transition
の始点、終点はmax-height
、max-width
の値で計算される- そのため、実際の高さ、幅と
max-height
、max-width
の差が大きいほどtransition
の効果は不自然になる- 基本的に設定した
transition-duration
より短くなる(早くtransition
が終了する) transition-timing-function
もheight
、width
の始点、終点とズレるので綺麗に適用されない- サイズが小さくなる動きの場合、クリックなどのユーザアクションに対して反応が遅れる
- 基本的に設定した
- そのため、実際の高さ、幅と
簡単なので間に合わせでとりあえず動きを付けたいという時は良いかも知れない。
JavaScriptでコンテンツサイズを調べる
getComputedStyle
、getBoudingClientRect
などでコンテンツのサイズを調べる方法。
ここではgetBoundingClientRect
を使うようにする。(getComputedStyle
だとauto
の値がうまく取れなかった事があったはずだけどうまく再現できず)
ラッパー要素を用意する
コンテンツ全体を包む要素を用意し、position: absolute
を適用する。
この要素のheight
、width
にauto
を指定して、ドロップボックスがエキスパンドした状態などを維持させておき、常にコンテンツサイズに合わせておく。
このラッパー要素からgetBoundingClientRect
でサイズを取得してリサイズする要素に適用する。
今のところこの方法が一番良さそうに感じる。
auto
の値を正しく得るために、外側のheight
、width
が固定値の場合はそれをラッパー要素にも適用する。
// Stylus .foo width 200px height 0 transition height .2s .bar position absolute width @width height auto
// JavaScript const foo = document.querySelector('.foo'); const bar = document.querySelector('.bar'); document.documentElement.addEventListener('click', e => { if(foo.classList.contains('opened')) { foo.classList.remove('opened'); foo.style.height = ''; } else { const contentHeight = bar.getBoundingClientRect().height; foo.style.height = `${contentHeight}px`; foo.classList.add('opened'); } });
唯一の欠点は、ドロップダウンメニューなどが開閉する際、コンテンツ自体はサイズが伸び縮みしないので、もしそういう風に動きを付けたい場合は、もう一手間必要になる、という点。
個人的な感覚では、コンテンツのサイズを伸び縮みさせたくないことのほうが多い(トランジションの途中で文字の折り返し部分が変わっていったりするとキモい)ので、あまり困らないかなと思う。
transition
を一旦無効にしてサイズを調べる方法
やってみたけど、ボツにした方法。
あえて利点を挙げるとすれば、コンテンツがテキストノードだけの場合でも使えるので、ラッパー要素を用意する必要が無い。
ブラウザ間の誤差を気持ち悪いハックで誤魔化しているので、いつ動かなくなっても驚かない。
使わないほうが良い。
- インラインスタイルで
transition
にnone
を設定 height
、width
にauto
を適用getBoundingClientRect
でheight
、width
を調べるheight
、width
を元の値に戻すtransition
を元の値に戻すsetTimeout
で非同期にheight
、width
に適用する
FireFox、ChromeではsetTimeout
で非同期に適用しないとtransition
が効かない、またChromeでは第二引数を0
にすると効かないことがある。
Safariでは同期処理中にもチラつきがある。
また、元の値を0
以外にすると、auto
の値を適用した時に、一度0
になってしまう。
// Stylus .foo width 200px height 0 transition height .2s
// JavaScript const foo = document.querySelector('.foo'); document.documentElement.addEventListener('click', e => { if(foo.classList.contains('opened')) { foo.classList.remove('opened'); foo.style.height = ''; } else { foo.style.transition = 'none'; foo.style.height = 'auto'; const contentHeight = foo.getBoundingClientRect().height; foo.style.height = ''; foo.style.transition = ''; setTimeout(() => { foo.style.height = `${contentHeight}px`; foo.classList.add('opened'); }, 30); } });
今日の結論
height
、width
にauto
が設定してある要素にtransition
を適用したい場合は、ラッパー要素を用意してJavaScriptでサイズを取得するのが良いと思う。
CSSだけだと厳しそうなので、JavaScriptで少しカバーしてあげるとバランスが良い。