映画のフィルムのような枠線を作る( `linear-gradient` 使用、高さ可変・レスポンシブ対応)

実現したいこと

まれによくあるシチュエーションですが、 border: 1px dashed #333;border: 1px dotted #333; のような border の指定ではなく、線の太さや間隔をカスタマイズした破線を作成したい、という状況です。

デモ

カスタマイズした破線でフィルムを表現
カスタマイズした破線でフィルムを表現

イメージとしては上述のフィルムを表現している部分のようなパーツです。これを画像ではなくcssでカスタマイズした破線にしたい、と。

コード

色々調整して、今回のデモのコードは以下のような形になりました(※該当パーツ部分だけ記載)。

html

<dl class="filmlist">
    <dt>
        <div class="filmlist_container">Louis Aime Augustin Le Prince</div>
    </dt>
    <dd>
        <div class="filmlist_container">Leeds Bridge</div>
    </dd>
    <dt>
        <div class="filmlist_container">Thomas Alva Edison</div>
    </dt>
    <dd>
        <div class="filmlist_container">35mm film</div>
    </dd>
    <dt>
        <div class="filmlist_container">Karel Zeman</div>
    </dt>
    <dd>
        <div class="filmlist_container">Vynalez zkazy</div>
    </dd>
</dl>

Scss

@charset "utf-8";

$dt-width: 350px;
$old-color: #F7E2B5;
$black-color: #000;
$square-width: 20px;
$square-height: 10px;
$square-margin: 12px;

@mixin lgDash() {
    content: "";
    position: absolute;
    top: #{$square-margin / 2};
    height: calc(100% - #{$square-margin / 2});
    width: $square-width;
    margin: auto;
    background: {
        image: linear-gradient(to bottom, $white-color, $white-color $square-height, transparent $square-height, transparent #{$square-height + $square-margin});
        size: $square-width #{$square-height + $square-margin};
        repeat: repeat-y;
    }
}

.filmlist {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    &_container {
        height: 100%;
        width: 100%;
        position: relative;
    }
    dt,
    dd {
        margin-bottom: 0.5rem;
        display: flex;
    }
    dt {
        width: $dt-width;
        .filmlist_container {
            border-left: #{$square-width + 2 * 2} solid $black-color;
            &::before {
                left: #{-1 * $square-width - 2};
                @include lgDash();
            }
        }
    }
    dd {
        width: calc(100% - #{$dt-width});
        .filmlist_container {
            border-right: #{$square-width + 2 * 2} solid $black-color;
            &::after {
                right: #{-1 * $square-width - 2};
                @include lgDash();
            }
        }
    }
}

ここまでは良かったのですが、一つ課題が残ってしまいました。

普通に作成すると破線の開始を top: 0px; にしても問題ないと思うのですが、下図のように今回のフィルムのような表現の場合で上端がないとややカッコ悪い気が……。

カスタマイズした破線でフィルムを表現(調整なし)
カスタマイズした破線でフィルムを表現(調整なし)

これをどうにかできないかとごにょごにょ弄った結果が上述のコードになります。さらに、以下のことも頑張ってみました。

  1. mixintop: #{$square-margin / 2};height: calc(100% - #{$square-margin / 2}); の部分で上端を必ず破線の余白(黒い部分)で開始する
    • 今回の場合は破線と破線の間は 12px としているので、 top: 6px; でずれ、その分 height も削りました
  2. 下端も同様に破線の余白で終わるように、「要素の高さがワンセット(破線自身(10px)とその余白(12px)の和(22px))の値で割り切れない場合は、現在の高さに最も近い22pxの倍数値に height を調整」する
    • 一度レンダリングした結果から算出するためjQueryを使用( load イベントにトリガー)
  3. テキストが入る部分の高さは可変。固定高ではない
  4. ブラウザ幅でテキストの折り返しが発生する可能性があるので、レスポンシブ対応として上述の下端を調整するJSを resize イベントにもトリガー
  5. 今回は dl + dt + dd で頑張ってしまったので dtdd で高さが揃わない可能性があります。そこで dt ならば隣接する直後の dddd ならば隣接する直前の dt の高さと自身の高さを比較して高さが揃うように調整

と、諸々を調整しようとして以下のjQueryが追加になりました。

$(window).on('load resize', () => {
    const squareHeight = 10;
    const squareMargin = 12;
    $('.filmlist_container').each(function (idx, elm) {
        $(this).css('height', 'auto');
        const elmHeight = $(this).outerHeight();
        const elmHeightSho = $(this).outerHeight() % (squareHeight + squareMargin);
        const tagName = $(this).closest('dt, dd').prop('tagName').toLowerCase();
        const elmNewHeight = elmHeight + (squareHeight + squareMargin) - elmHeightSho;
        let newHeight = 0;
        if(tagName === 'dt') {
            const ddHeight = $(this).closest('dt').next('dd').outerHeight();
            if(elmNewHeight > ddHeight) {
                newHeight = elmNewHeight;
            }
            else {
                newHeight = ddHeight;
            }
        }
        else {
            const dtHeight = $(this).closest('dd').prev('dt').outerHeight();
            if(elmNewHeight > dtHeight) {
                newHeight = elmNewHeight;
            }
            else {
                newHeight = dtHeight;
            }
        }
        if(elmHeightSho !== 0) {
            $(this).css('height', `${newHeight}px`);
        }
    });
});

いや、細かいところまで気にしてしまうと思ったよりも沼ですねこれ……軽率に書き始めたので反省。

参考

linear-gradient

display: flexbox; で高さを揃える

(未使用)jQueryから疑似要素へのアクセス

ただしgetのみ。

この記事を書いた人

アバター

アルム=バンド

フルスタックエンジニアっぽい何か。LAMPやNodeからWP、gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。