coder's log

コーダー日誌

[CSS][jquery]flexboxの横並びレイアウトで子孫を揃える方法

公開日:2019/10/30

結論、cssだけでは(固定サイズを指定しない限り)flexboxの横並びレイアウトの孫要素までの高さ揃えはちょいと厳しいのですが・・・

  • このページはウィンドウサイズを変更したり、モバイルデバイスの向きを変更したりするとリロードします。

とりあえずチートシート&ジェネレーター

まずは、背景が揃わないパターン

画像とか

画像の説明とか

テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■

こんな感じで背景が揃わないよ!というときがあります。
この例ではhtmlは以下のような構成になっています。タグはコンテンツに合ったものを使えば何でも良いです。この例なら画像+画像の説明という妄想なので、figure>img+figcaptionという組み合わせのほうがよかったかもですね。

<div class="flex">
    <div>
        <p>コンテンツ1</p>
        <p>コンテンツ2</p>
    </div>
    <p>コンテンツ3</p>
</div>

この時点では親要素のclass[flex]には以下のようなcssが記述されています。
なお、子要素にはwidthを設定してあります。

display: flex;
align-items: center;

垂直方向を揃えるalign-itemsの初期値はstretchなので、親要素にalign-itemsを設定しなければ子要素の垂直方向の領域は親要素の高さに合わせて可変されます。
しかし、この例のように垂直方向で中央揃えにするために親要素のalign-itemsに初期値以外を設定すると、子要素はコンテンツ量に合わせて領域を確保するので、子要素に設定された背景は揃いません。
これを下のように背景を揃えたいと思います。

画像とか

画像の説明とか

テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■

まずは、親要素のalign-items: centerを削除します。
それだけでは、親要素がalign-items: stretch状態になるだけなので、子要素の方にdisplay: flexとalign-items: centerを設定して、位置を調整します。

.flex{ display: flex; }

.flex > *{
    display: flex;
    align-items: center;
}

.flex > *:first-child{ flex-direction:column; }

.flex > *:first-child > *{ width:100%; }

なお、子要素にflexboxを設定することによって、孫要素が横並びになってしまうので、この例ではflex-directionを使って配置を調整しています。
widthもstretch状態になってしまうので、100%にしました。
ちなみに背景などの設定は省略してあります。適宜設定してください。

画像+テキスト(等)の横並びリンクリストの高さを揃えたい

最近は、スマートフォンなどのモバイル端末でwebサイトを利用するユーザーも多いことから、タップしやすいようにリンクエリアを大きくとる傾向があります。
よくあるパターンとしては、下のようにリンク先のイメージ画像+説明文をひとまとめにリンク設定するものです。これをリストとして横並びにレイアウトしているものもよく見かけます。
なおこの例では、更に右下にリンク先のサイト名を入れてあります。

unsplash-logofabe collage

  • 画像の前提として、縦横サイズはバラバラだとしても縦横比率は同じで、横サイズはliの幅以上とします。
  • ここで使用している画像は以下を参照。画像の使用はリンク先の利用規約に従ってください(日本語の説明がないので以下の記事も参考にしてください)。

htmlは以下のようになっています(長くなるのでliは1つに省略してあります)。

<ul class="flex">
    <li>
        <a href="URL" target="_blank">
            <img src="画像のパス" alt="画像の説明">
            <p>リンク先の説明</p>
            <span>サイト名</span>
        </a>
    </li>
</ul>

例を見てもわかるように横並びのリンクリストの高さが揃っていません。
liタグにborderを設定すれば見た目は揃うのですが、ここではわかりやすいように、aタグにborderを設定してあります。
つまり、liタグの方にborderを設定して見た目はごまかせても、実際は縦の長いリンクと同列の短いリンクの下の方のリンクエリアと思しき空間をタップ(クリック)してもリンク先にジャンプしない事件が発生します。
この状態の親要素には以下のcssが設定してあります。

display: flex;
justify-content:space-between;
  • justify-content:space-between(均等割付)を設定せず、liとliの間隔をmarginで調整しても良いです。
    例えばliの幅は等幅で1行3列均等割付にした場合、liが5つあったとすると、2行目のliは左右に分かれるので、真ん中に妙な空間が空いてしまいます。
    liの数が可変ならば、好みやデザインにもよりますが、均等割付を使わない方が無難かと思います。

先の背景が揃わないflexboxレイアウトを修正した方法を思い出すと、なんとなくどうすれば下のようになるのかわかると思います。

今回は親要素のcssに変更は加えません。子要素のliタグにdisplay: flexを追加するだけです。
これだけでliの子要素であるaタグにalign-items: stretchが効きます(align-itemsを記述しなくてもいいのはstretchがデフォルト値だからです)。

.flex{
    display: flex;
    justify-content:space-between;
}

.flex > li{ display: flex; }

ここまでくると、各リンクにある右下のサイト名の位置も揃えたくなりますよね?
上のcssに加えて孫要素のaタグにもdisplay: flexを設定し、サイト名(この例ではspanタグ)にmargin-top:autoを設定すると実現できます。

.flex > li > a{
    display: flex;
    flex-direction: column;
    width:100%; /* IE11のバグ対策 */
}
.flex > li > a > span{
    margin-top: auto;
    text-align: right;
}

ポイントはaタグにdisplay: flexを設定すると、その子要素たちは横並びになってしまうので、flex-directionを使って縦並びに変更することです。
なお、flexを入れ子にしてflex-directionを使ったりすると、IE11(など)で幅がコンテンツに合わせて収縮してしまうようなので、width:100%も追加しておきます。

ちなみに、この例で使っている画像の縦横サイズは3680×2456pxあるものの、親要素の幅に合わせて縮小されて表示されます。
imgにwidth:100%、height:autoが設定されているためですが、通常、これでIE11でも親要素の幅に合わせて縦横比率を保ったまま縮小表示されます。
しかし、flexboxでflex-directionを使用した際など、縦横比率が維持されなくなるバグが発生することがあります。
その場合は、imgタグにflex-shrink: 0(またはmin-height: 0%)を設定すると解消されます。
このバグはEdgeでも発生することがあるようです。

.flex img{
    width:100%;
    height:auto;
    flex-shrink:0; /* またはmin-height: 0%; */
}

横並びで子要素・孫要素を揃える

ここまでやってみると、子孫のブロックがいくつあっても高さの可変に関係なく横位置を揃えられそうな気がしてきます・・・。

下のような横並びflexboxで試してみようかと思います。今度はリンクリストではありません。

  • タイトル□タイトル■

    テキスト□

    旨い魚たべたい
  • タイトル□タイトル■タイトル□タイトル■タイトル


    ひらめ

    テキスト□テキスト■テキスト□テキスト■テキスト□

    旨い魚たべたい
  • タイトル□タイトル■

    テキスト□テキスト■テキスト□テキスト

    旨い魚たべたい

htmlは以下のような構成です(長くなるのでliタグは1つに省略してあります)。
タグは何を使用しても良いですが、この例ではspanとiにdisplay:blockを設定してあります。

<ul class="flex">
    <li>
        <p>タイトル□タイトル■</p>
        <span>テキスト□テキスト■</span>
        <p>テキスト□テキスト■</p>
        <i>テキスト□テキスト■</i>
    </li>
</ul>

この状態の親要素には以下のcssが設定してあります。

display: flex;

このような例の場合、spanとiの位置を揃えたくなります。

  • タイトル□タイトル■

    テキスト□

    旨い魚たべたい
  • タイトル□タイトル■タイトル□タイトル■タイトル


    ひらめ

    テキスト□テキスト■テキスト□テキスト■テキスト□

    旨い魚たべたい
  • タイトル□タイトル■

    テキスト□テキスト■テキスト□テキスト

    旨い魚たべたい

(PCでは)なんとなく揃って見えますが・・・。とりあえず、cssは以下のようになっています。
なお、liタグのwidthやmargin、その他装飾的なcssは省略してあります。

.flex{ display: flex; }

.flex > li{
    display: flex;
    flex-direction: column;
}

.flex > li >*:not(:last-child){
    height: 100%;
    min-height: 0%;
}
.flex > li >*:last-child{
    margin-top: auto;
    text-align: right;
    height: 4em;
}

上の「背景が揃わないパターン」や「画像+テキスト(等)の横並びリンクリストの高さを揃えたい」の応用なので、これといって変わったことはしていないのですが、class[flex]の孫要素にheight:100%が設定しているところがミソです。

flexboxの初期設定なので省略されていますが、class[flex]の子要素・孫要素にはalign-items: stretchが効いているので、同列の各子要素・孫要素の高さ100%に合わせて伸縮されます。
ただし、各liタグ(ここでは子要素)の右下に揃えたいiタグ(末尾の孫要素)だけは伸縮させないで、margin-top: autoを設定しています(表示領域確保のために適当な高さも設定)。

だがしかし!

  • タイトル□タイトル■

    テキスト□

    旨い魚たべたい
  • タイトル□タイトル■タイトル□タイトル■タイトル


    ひらめ

    テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■

    旨い魚たべたい
  • タイトル□タイトル■

    テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■

    旨い魚たべたい
  • タイトル□タイトル■

    テキスト□

    旨い魚たべたい

なにかおかしい・・・

SPなどで見るとさらに顕著なのですが、同列のコンテンツ量が極端に違ったりすると、この例で言うところのタイトルが入ったpタグのエリアやspanのエリアが間延びしたり、下のテキストがliタグの領域からはみ出たりしてしまうのです。

これは、align-items: stretchによって、liタグ(子要素)の領域を孫要素が分け合っているために起こる現象です。
実は子要素の領域は、孫要素にheight:100%が設定されていない本来の高さの合計で確保されています(とはいえ子要素もalign-items: stretchなので、高さは同列の一番高いliとおなじになっています)。
なので、孫要素の高さを100%にして高さが拡張されてしまうと、liの領域からはみ出てしまったりするわけです。

それならば、flex-growを使えばなんとかなるんじゃないの?と思いたくなりますが、heightが100%なので、効きません。

というわけで、孫要素の高さが可変する条件で、cssだけでflexboxのと横並びレイアウトの孫要素まで隣の位置に合わせようというのは厳しいです。

孫要素の高さを固定する(テキストなら規定の文字数数にする)などの制限をし、配置を工夫すれば(高さが変化しない要素を上に配置するなど)、先にご紹介した横並びリンクリストのように多少の可変に耐えます。

jqueryを使うという選択肢

横並びのflexboxで子要素・孫要素を揃えたいという欲求を叶えるためにcssをこねくり回すよりも、jsなどに頼ってしまった方が納期に間に合うかもよ!というわけで、簡単なものをjqueryで作ってみました。

  • タイトル□タイトル■

    テキスト□

    旨い魚たべたい
  • タイトル□タイトル■タイトル□タイトル■タイトル


    ひらめ

    テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■

    旨い魚たべたい
  • タイトル□タイトル■

    テキスト□テキスト■テキスト□テキスト■テキスト□テキスト■

    旨い魚たべたい
  • タイトル□タイトル■

    テキスト□

    旨い魚たべたい

htmlの構造は変更ありません。
cssについては親要素にdisplay: flexを設定してますが、子孫要素には設定していません(その他、幅や装飾的なものは省略しています)。
また、今回は右下に表示させたい要素はmargin-top:autoを使わずに、position: absoluteを使って配置しています。

.flex{ display: flex; }

.flex > li{
    position: relative;
    padding: 10px 10px 3em;
}

.flex > li > i{
    position: absolute;
    bottom:10px;
    right: 10px;
}

とりあえず、変数を宣言します。

$(function(){
    var liObj = '.flex li';
    var liL = $(liObj).length;
    var l = $(liObj).children().length / liL;
    var colNum = Math.floor($('.flex').width() / $(liObj).width());
    var rowNum = Math.ceil(liL / colNum);
    var cBase = f = 0;
    var maxHeight = new Array(l);
})
  • liObj:頻繁に使用する要素を格納(この例ではliタグ)
  • liL:liの個数を格納
  • l:liの子要素(class[flex]から見ると孫要素)の数を格納・・・孫要素のタグは複数種類あることに注意
  • colNum:1行あたりのliの個数を格納
  • rowNum:横並びのリストの行数を格納
  • cBase:liの数を(行ごとに)カウントする変数
  • f:ループをbreakするかを判定するためのフラグ
  • maxHeight:liの各子要素の高さの最大値を格納する配列

以下のコードは$(function(){~})内に書きます。

リスト全体で、liの各子要素の高さの最大値に全体の高さを合わせる場合は以下のようになります。

//行ごとに高さ調節しなくても良い場合
for(i=0; i<l; i++) maxHeight[i] = 0;

$(liObj).each(function(){
    for(i=0; i<l; i++){
        if($(this).children().eq(i).height() > maxHeight[i]) maxHeight[i] = $(this).children().eq(i).height();
    }
});

$(liObj).each(function(){
    for(i=0; i<l; i++){
        $(this).children().eq(i).height(maxHeight[i]);
    }
});

maxHeightの配列に初期値を与えたあと、liの各子要素の高さを比較して最大値を格納し終えたら、統一の高さをそれぞれに設定し直しています(ちなみにこの場合は変数colNumとrowNum、cBase、fは使用しません)。

ただ、これだと、例えば1行目のliの子要素(のどれか)のテキストが極端に多く、2行目のliの各子要素は全体的にテキストが少ないといった場合、2行目が間延びしたようになってしまうので、高さの比較を行ごとに行うように変更してみます。

//行ごとに高さ調節する場合
for(r=0; r<rowNum; r++){
    for(i=0; i<l; i++) maxHeight[i] = 0;
    cBase = r * colNum;

    for(c=0; c<colNum; c++){
        for(i=0; i<l; i++){
            if($(liObj).eq(cBase + c).children().eq(i).height() > maxHeight[i]) maxHeight[i] = $(liObj).eq(cBase + c).children().eq(i).height();
        } 
    }

    for(c=0; c<colNum; c++){
        for(i=0; i<l; i++){
            $(liObj).eq(cBase + c).children().eq(i).height(maxHeight[i]);
        }
        if((cBase + c + 1) >= liL) {
            f = 1;
            break;
        }
    }
    if(f) break;
}

一行ごとにmaxHeightの配列をリセットし、先ほどと同様にliの子要素の高さをその行で最大のものに書き換えています。
一応、liの総個数分だけ作業をしたらループをbreakさせていますが、breakしなくても問題ないかと思います。

最後に、SPなどでデバイスの向きを変えたときに、リロードして表示し直すようにします。

//向き(画面・ウィンドウサイズ)が変わったらリロード
var lastInnerWidth = window.innerWidth;
$(window).on('resize', function() {
    if ( lastInnerWidth != window.innerWidth ) {
        lastInnerWidth = window.innerWidth ;
        setTimeout('location.reload();', 100);
    }
});

もう少し短いコードにできるかもですが、なんとなく仕組みがわかりやすいのはこんな感じかな・・・と思いつつ書いてみました。
リロードについても、ページ全体をリロードするのではなく、外部ファイルにして該当部分だけを再読み込みさせることもできると思うので、試していただけると幸いです。
jqueryのメソッドなどについては以下のwebページなどを参考にしてみてください。

  • ここではリサイズイベントでリロードさせていますが、OSやブラウザ(そのバージョンや組み合わせ)によってはリロードのたびにページトップへ移動してしまいます。
    人によってはうざいと感じると思いますので、可能であればページ全体ではなく、この部分だけ更新できるようにしたほうが良いでしょう。
  • サイトの運用ルールでajaxなどの使用を禁止されているという企業にも遭遇することがあったので、一応上のリンクの最後に、リロード前の位置を保管してリロード後に表示する例を掲載してみましたが、モバイル環境では位置がずれそうな気もします。どうだろう・・・何らかの計算は必要かもしれませんね。

他にも世の中には素敵なコードやプラグインを提供してくださっている方々がたくさんいらっしゃいますので、google先生たちに訊いてみてください。

おまけ

他にもflexboxを使った素敵なレイアウトがあります。
下のリンクのように様々なサンプルや事例を紹介してくださっているサイトや記事があるので、こちらもgoogle先生たちに訊いてみてください。

また、ブラウザ(のバージョン)によるバグなども報告されています。
古いバージョンばかりではなく、今まで問題なかったのにバージョンアップしたらバグが追加された(なぜ!!)ということもあるので、コードは間違ってないのに表示が崩れるといったときは、慌てずgoogle先生たちに訊いてみると良いと思います。
バグが修正されたからこそ、表示が崩れるってことあるかもですが・・・。

もひとつおまけ・・・display: grid

ブラウザのバージョンによっては使えず、業務では使用を避けていたので、ご紹介だけになりますが・・・。
CSSグリッドレイアウトを使うという方法もあります。

2019/10現在、各メジャーブラウザの最新バージョンでは大抵使えるようになったようです(一部ベンダープレフィックスが必要だったりします)。
そのうち、色々試してみて具体的な使い方や注意点などをご紹介できたら良いなと思っています。

更新記録
  • 2019/11/01 リロードに間する際の注意点追記

Top

go to homeindex