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で少しカバーしてあげるとバランスが良い。
Stylusのあまり知られていない機能
というより、自分が最近知った機能。
ハッシュをCSSのプロパティ名と値に展開できる
Stylusではハッシュが使えます。
シンタックスはJSのオブジェクトリテラルとほぼ同じですが、Stylusのnodeをそのまま値に使用できる点が違います。
以下のように、値に10px
と書いてもvalidです。
foo = { width: 10px } // JSと同じようにアクセスできる bar = foo.width bar = foo['width'] // 代入もできる foo.height = 20px foo['height'] = 20px
ハッシュを{foo}
のようにブレースで囲むと、ハッシュのキーバリューがそのままStylusとして解釈され、CSSのプロパティと値に展開されます。
ハッシュがネストになっている部分は、Stylusのネスト(インデント)として解釈されるので、以下のように&
は親セレクタとして展開されます。
foo = { width: 10px, height: 20px, '&:hover': { color: crimson } } .bar {foo} // こう解釈される // .bar // width 10px // height 20px // &:hover // color crimson // => .bar { // width: 10px; // height: 20px; // } // .bar:hover { // color crimson // }
注意点としてネストされたハッシュや、関数が返すハッシュをそのまま展開しようとするとエラーになります。
いったん変数に代入するとうまくいきます。
foo = { bar: { opacity: 1 } } .baz {foo.bar} // エラー .baz {foo['bar']} // エラー qux = foo.bar .baz {qux} // => .baz { // opacity: 1; // }
JSONを読み込んで色々できる
json
というビルトイン関数を使用して、JSONを読み込むことができます。
キーバリューがそのまま変数名と値として展開され、変数が定義されます。
ネストされている場合は、各階層のキーが-
で連結された物が変数名になります。
{ "foo": "10px", "bar": { "baz": "20px", "qux": "'Helvetica Neue'" } }
上のJSONを読み込むと以下のように変数定義が行われます。
注意点として、ダブルクォートはトリムされるので文字列を値にしたい場合は内側でさらにシングルクォートで囲うなどする必要があります。
json('./vars.json') // foo = 10px // bar-baz = 20px // bar-qux = 'Helvetica Neue' .title font-family bar-qux // => .title { // font-family: 'Helvetica Neue'; // }
json
関数には以下のオプションが用意されています。
hash
leave-strings
optional
hash
JSONをそのままハッシュとして読み込みます。
vars = json('./vars.json', { hash: true }) p(vars.foo) // => inspect: 10px p(vars.bar.qux) // => inspect: 'Helvetica Neue'
leave-strings
ダブルクォートをトリムしないようにする、読み込まれる値が全て文字列になります。
leave-strings
は実際にはhash
オプション前提のようで、hash
をつけなくてもハッシュとして読み込まれます。
vars = json('./vars.json', { hash: true, leave-strings: true }) p(vars.foo) // => inspect: '10px' p(vars.bar.qux) // => inspect: ''Helvetica Neue''
optional
jsonファイルが存在しなくてもエラーにならない。
こちらもhash
オプション前提で、hash
をつけなくてもハッシュとして読み込まれます。
vars = json('./nothing.json', { hash: true, optional: true }) p(vars) // => inspect: null
JavaScriptの関数を呼べる
こうしてStylusの中でハッシュも配列も関数もあって、文字列操作もそれなりに用意されていると、だんだん複雑な関数を作り始めたりします。
とはいえ、JSで書けたら楽なのになーという場面も結構あります。
そんな時にもStylusにはJavaScriptを呼び出すためのAPIが用意されています。
JavaScript側では以下のようにプラグインを作っておき、Stylus側ではuse
関数を使って読み込みます。
// add.js module.exports = () => stylus => { stylus.define('add', (a, b) => a.operate('+', b)); };
use('path/to/add.js') res = add(10px, 20px) // => 30px
CLIの場合でも-u
オプションで同じように使用できます。
$ stylus -u ./path/to/add.js src/index.styl -o dist
Stylusのノードを作って返すようなこともできるみたいです。
この辺ドキュメントがあまり詳しく書かれてないのでよく分かってないです、またいずれ調べてみます。
nibはおそらくこんな感じでJavaScriptのAPIからnode-canvasを呼び出してgradientをpng画像に変換したりしてるんだと思います。
他にも公式ページを見ていると色々知らない機能が出てくるので面白いです。
button要素にはflexboxを使わないほうが良さそうです
<button>
要素にflexboxを適用したところ、Chrome、Safari、FireFoxでそれぞれ表示が違った。
IEではまだ試していない。
調べてみるとどうやらこういう事らしい。
stackoverflow: Flexbox not working on <button>
element in some browsers
The problem is that the
<button>
element cannot be a flex container (in some browsers).Certain HTML elements, by design, do not accept display changes. Three of these elements are:
<button>
<fieldset>
<legend>
The idea is to maintain a level of common sense when styling HTML elements. For instance, the rule prevents authors from turning a fieldset into a table.
button
要素はflex containerにはなれないと。
上記三つの要素はCertain HTML elements
(用途が明らかな要素?)なので、display
の値は変えられないよ、というような事が書いてあるような気がします。
デモ
Chromeだと他の通常のブロック要素と同じようにflexboxが適用される。
Safariだとdisplay: flex
、flex-flow: row nowrap
は効いてるっぽいが、justify-content
が効かない。
FireFoxだとそもそもdisplay: flex
が効かない。
// jade button i.material-icons looks_one i.material-icons looks_two .button i.material-icons looks_one i.material-icons looks_two
// stylus @import 'nib' button, .button size 200px 50px padding 10px margin 20px text-align center box-sizing border-box display flex // FireFoxで効かない flex-flow row nowrap justify-content space-around // Safariで効かない outline none border none background-color #3399aa .material-icons size 30px auto line-height 30px background-color #bbb
各ブラウザのスクリーンショットこんな感じです。