メモを揉め

お勉強の覚書。

Case Studies in Flexbox - flex-grow, flex-shrink, flex-basis

flexboxのプロパティの組み合わせを比較できるカタログが欲しいなーと思ったので作ってみました。
各プロパティーの値がどのように解釈されるか、ブラウザ間での違いなどが分かります。

あれもこれもと入れていたら無駄に長くなりました。

2017年8月17日追記

Safariflex-basis の挙動が ChromeFirefox、Edge と同じなっていたので、記事とサンプルをアップデートしました。

See the Pen Case Studies of Flexbox by Keita Okamoto (@all-user) on CodePen.

縦のストライプの幅が10pxflexコンテナの幅が360pxとなっていて、flexアイテムとそのコンテンツの大きさはそれぞれのラベルに表示されます。

コンテナの上には当てているスタイルの要約と最終的なサイズを割り出す計算式があります。
全ブラウザで結果が同じ場合は緑、ブラウザ間で結果に差異がある場合は赤の文字になっています。

f:id:alluser:20160828194455p:plain f:id:alluser:20160828194518p:plain

一応仕様的にはこうなるべきだろうという計算式を書いているつもりですが、解釈が間違っているかも知れません。
なんか違うなと思ったら教えていただけると助かります。

ベストプラクティス(暫定)

flexboxの各アイテムが最終的にどのサイズで描画されるのかは領域の分配ルールに様々な要素が絡んでいます。
そのため、適当にプロパティを組み合わせていくと、なかなか思い通りのサイズになりません。

スコープを絞るために話をflex-direction: rowに限定するとして、サイズの計算に影響を与えるプロパティはこれだけあります。

  • flex-shrink
  • flex-grow
  • flex-basis
  • width
  • min-width
  • max-width
  • margin
  • padding
  • box-sizing

さらにブラウザ間の誤差やflexboxの抱える様々なバグを考慮して全てをコントロールするのはかなり難しいと思われます。(この実験に関して言えば、実際にはブラウザの誤差はあるひとつの挙動に起因していたため、そこまで辛くなかった。後述)

色々試した結果、暫定的なベストプラクティスとして以下のルールを守るのが良さそうだという結論になりました。

  • flex-basisauto(初期値)
  • width0に設定する(ただしflex-direction: column + heightの時は要注意。下に追記あり)
    f:id:alluser:20160828222041p:plain
  • width0以外にしたい場合はmin-widthをそれより小さい値で設定する
    f:id:alluser:20160828223132p:plain

min-widthを設定すればflex-basisでもブラウザ間の差異は無くなりますが、あえてflex-basisを使うメリットも特に無さそうなのでwidthを使うのが良いかなと思ってます。

  • flex-basisとmin-widthを組み合わせた例
    f:id:alluser:20160828223924p:plain

あくまで厳密にサイズをコントロールしたい時のベストプラクティスです。(flexboxでテーブルみたいに縦を揃えたい、とか)
コンテンツのサイズが予測できたり、サイズに応じてよしなにやって欲しい時はwidthflex-basisともにauto(必要に応じてmax-widthmin-width)でも良いと思います。

幅の計算をするときに気をつけること

paddingmarginの扱い

paddingmarginflex-shrinkflex-growの影響を受けないようです。
常に設定された値になる必要があるので、スペースを分配した結果が設定した値を下回る場合はスペースを再分配する必要があります。

  • 左右のpadding 80pxを下回るため再分配が行われる
    f:id:alluser:20160828223132p:plain

box-sizingの影響

paddingwidthに含まれるかどうかはbox-sizingの値によって決まります。
初期値であるcontent-boxの場合は以下のようになります。

  • width: autoのためコンテンツサイズ分の40px * 3を除く240pxflex-growの値に基いて分配される
    f:id:alluser:20160828231139p:plain

  • コンテンツサイズ分の40px * 3に加え、左右のpadding 40px * 3を除く120pxflex-growの値に基いて分配される
    f:id:alluser:20160828230822p:plain

  • flexアイテムのwidth 160px * 3とコンテナサイズ360pxの差-120pxflex-shrinkに基いて分配される
    f:id:alluser:20160828234942p:plain

  • flexアイテムのwidth 160px * 3に左右のpadding 80px * 3を加えた720pxとコンテナサイズ360pxの差-360pxflex-shrinkに基いて分配される
    f:id:alluser:20160828223132p:plain

min-widthの効果

widthの設定を0以上に設定していて、その値よりコンテンツサイズが小さい場合、flex-shrinkはコンテンツサイズを下回って縮小することができなくなります。(flex-basisの場合は0に設定してもコンテンツサイズを下回るサイズにはなれないことがある。後述)

min-widthを設定することで、その値まではコンテンツサイズを下回るサイズに縮小することが出来ます。

  • コンテンツサイズの160pxを下回って縮小することが出来ない(Safari、IE11は縮小する) f:id:alluser:20160829001831p:plain

  • min-width: 0によりコンテンツサイズの160pxを下回って縮小することが出来る
    f:id:alluser:20160829001849p:plain

ブラウザ間の差異(2016年8月28日現在)

この実験に絞って言えばブラウザ間で結果に差異が出る原因は、flex-basisがコンテンツサイズを保証するかどうか、ということに集約できました。

ChromeFirefox、Edgeはコンテンツのサイズを維持。
Safari、IE11はコンテンツサイズを無視する。

コンテンツサイズを保証するというのはどういう事かというと、flex-shrinkflex-growを適用した後に、もしflexアイテムのサイズがコンテンツサイズを下回っていた場合に、そのflexアイテム以外のflexアイテムの間でネガティブなスペースを再分配し、必ずコンテンツサイズを下回らない様にします。(それでも下回る場合はそれ以上縮小しない)

  • 再分配してもコンテンツサイズを下回るのでそれ以上縮小しない例
    f:id:alluser:20160828202921p:plain

どちらが仕様的に正しいのかはわかりませんが、IEを除くとコンテンツサイズを無視するのはSafariだけなのでChromeFirefox、Edgeの動作がより最新の仕様に沿っているのかもしれません。
現在は SafariChromeFirefox、Edge と同じ動作です。

追記(2016年8月31日)height0指定について

flex-direction: column時にheight0を指定すると、たとえflexアイテムがflex-growによってコンテナいっぱいに広がっていたとしても、flexアイテム内の要素からはheight0に見えてしまうらしく、%指定が出来なくなるというデメリットがあることが分かりました。(Safariのみ)

heightの代わりにmin-height0にすれば大丈夫でした。

  • Safariのスクショ。左: height: 0、右: height: 100% + min-height: 0
    f:id:alluser:20160830235946p:plain
    枠線付きがflexコンテナ、濃いグレーがflexアイテム、薄いグレーがflexアイテム内のブロック要素

http://codepen.io/all-user/pen/RGbKRd

追記(2016年9月5日)flex-basiscontent について

flex-content に指定できる値 content についての調査を追加しました。

結論から書くとブラウザ間の挙動がバラバラ。
今はまだ使わないほうが良さそうです。

以下、実験で分かったこと。

ショートハンドが効かない(Chrome, Firefox, Safari, IE11)

ショートハンドで指定した flex-growflex-shrink がなぜか無効になります。(Edge は有効)
flex-grow0Chrome, Firefox, Safari, IE11)、 flex-shrink1Chrome, Firefox, Safari)と解釈されているように見えます。

ロングハンドの指定は有効でした。

  • flex-grow
    • flex-basiscontent
      flex-grow123 をショートハンドで指定
      f:id:alluser:20160905233121p:plain
    • flex-basiscontentcontentauto
      flex-grow123 をショートハンドで指定
      f:id:alluser:20160905234039p:plain
    • flex-basiscontent
      flex-grow123 をロングハンドで指定
      f:id:alluser:20160905235419p:plain
  • flex-shrink
    • flex-basiscontent
      min-width0
      flex-shrink123 をショートハンドで指定
      f:id:alluser:20160906001341p:plain
    • flex-basiscontentcontentauto
      min-width0
      flex-shrink123 をショートハンドで指定
      f:id:alluser:20160906001658p:plain
    • flex-basiscontent
      min-width0
      flex-shrink123 をロングハンドで指定
      f:id:alluser:20160906003231p:plain

追記(2016年9月6日)Firefoxの表記について

FireFox から Firefox に変更しました。

CSSの先頭に書くやつ

// Stylus

*
  position relative

リセット系CSSと併用することを前提にしています。
使用するライブラリにbox-sizingの設定がない場合は以下も追加します。

// Stylus

:root
  box-sizing border-box

*
  box-sizing inherit

box-sizingに関してはsanitaze.cssを参考にしています。

github.com

Eric Meyer’s Reset CSSnormalize.cssなどにはbox-sizingの設定は含まれていませんでした。

positionを設定しない時の問題

CSSのプロパティーの中にはpositionの値に依存しているものがいくつかあります。

positionの初期値はstaticという値ですが、
MDNの説明では、

  • static
    規定の振る舞いです。フロー内の現在の位置に配置されます。top、right、bottom、left、z-index プロパティは適用されません。
    MDN: position

とあります。

上記に加え、

  • overflow
    • scrollautoに設定してもスクロールしない
    • hiddenを設定してもはみ出した部分が表示される
    • つまりオーバーフローしていると見なされなくなる?
  • 子要素のwidthheightにパーセント指定
    • staticに設定された要素は無視される
    • 親を辿ってstatic以外に設定された要素の値から算出される

などの影響も受けます。

多分、他にもあると思います。
はっきり言って、あるプロパティーがうまく効かなかった時に、それがpositionのせいかどうか調べるのはめちゃくちゃしんどいです。

ていうかstaticってなんなんだほんとに。意味あんのか。

なので今は全ての要素にposition: relativeをあてています。

*を使った指定は何かと問題も引き起こしやすいですが、このrelativeの指定に関してはメリットのほうが大きいかなと思います。

flexアイテム内のスクロールを有効にする

やりたいこと

display: flex内の各要素(以下flexアイテム)は、自分の中のコンテンツに合わせて大きさが変わります*1
そのため、コンテンツが画面からはみ出すような大きさの時はスクロール表示になって欲しいなーって時でも、そのままではスクロール表示になりません。

flexアイテムの大きさはいい感じに分配されつつ、コンテンツがはみ出す時はスクロール表示になってほしいわけです。

試した結果これとほぼ同じ方法で出来ました。

memowomome.hatenablog.com

やり方

flexアイテム内のコンテンツをラッパーで包みposition: absoluteにする*2

// Jade

.foo
  .bar
  .baz
  .qux
    ul.wrapper
      li: i.material-icons looks_one
      li: i.material-icons looks_two
      li: i.material-icons looks_3
      li: i.material-icons looks_4
      li: i.material-icons looks_5
      li: i.material-icons looks_6

position: absolute

  • looks_one
  • looks_two
  • looks_3
  • looks_4
  • looks_5
  • looks_6

// Stylus

@import 'nib'

*
  position relative

.foo
  size 300px
  display flex
  flex-flow column nowrap
  .bar,
  .baz,
  .qux
    flex 1 1 auto
  .bar
    background-color #3399ac
  .baz
    background-color #cc3a20
  .qux
    background-color #bee
    overflow-y auto
    ul.wrapper
      absolute top 0 left 0 // ラッパーにposition: absoluteを適用する
      padding 10px

position: relative

  • looks_one
  • looks_two
  • looks_3
  • looks_4
  • looks_5
  • looks_6

// Stylus

@import 'nib'

*
  position relative

.foo
  size 300px
  display flex
  flex-flow column nowrap
  .bar,
  .baz,
  .qux
    flex 1 1 auto
  .bar
    background-color #3399ac
  .baz
    background-color #cc3a20
  .qux
    background-color #bee
    overflow-y auto
    ul.wrapper // position: absoluteの指定無し
      padding 10px

*1:max-height、max-widthが設定してある場合はその大きさまで

*2:コンテンツの要素が一つの場合はラッパーは必要無し