Programming Field

[Web] cqw / cqh の応用 - CSSのみで自動line-clamp計算・テキスト自動縮小を行う

ここでは、CSSで使用できる長さの単位「cqw」および「cqh」を使ったテクニックを紹介しています。これを使うことで「line-clamp」の自動計算やテキストの自動拡縮をCSSのみで行うことができます。

cqw / cqh とは: コンテナークエリー

cqw および cqh は、CSSにおいて「長さ」を指定する箇所で使用できる「単位」の1つで、「コンテナークエリー」(Container query)の機能を利用してコンテナーを「クエリーコンテナー」にした際に有効になり、cqw は対象コンテナーの幅の割合(100 が目いっぱいの幅)、cqh は同高さの割合となります。具体的には、ある要素が「container-type: size; height: 123px」となっている場合、その直下の子要素において「100cqh」という長さを用いると「123px」と計算されます。

※ ここではコンテナークエリーの詳細については省略します。詳しくは 「CSS コンテナークエリー - CSS: カスケーディングスタイルシート | MDN」などをご覧ください。

container-type」を使用した場合、その要素は幅や高さを明示的に指定する必要があり、指定しなかった場合「0」になる(ブロックやフレックスなどによって引き伸ばされる場合を除く)、という仕様が存在します。そのため、サイズを決定させるためのスタイル指定が必要になりますが、必ずしも px などの絶対値指定で行う必要はなく、たとえば「width: 100%; height: 100%」として親要素のサイズそのままにする、という指定も可能です。また、フレックスボックスなどのルールによるサイズ指定(引き伸ばし)を使うこともできます。

以下は、水色のボックスの縦横サイズに「100cqh」を指定していますが、親である「.mySubSubContainer1」のサイズを参照しており、このボックスはフレックスボックスの影響で緑色のボックスのサイズに引っ張られるため、この緑色ボックスのサイズを変更すると合わせて拡縮される形になります。

/* @supports を使うことで非対応ブラウザーの場合分けが可能 */
@supports (container-type: size) and (height: 100cqh) {
    .cssSample1NotSupported {
        display: none;
    }
}
.myContainer1 {
    display: flex;
    align-items: stretch;
    border: 1px solid;
}
.myBox1 {
    width: 80px;
    height: 100px;
    box-sizing: border-box;
    resize: vertical;
    background-color: #00ff00;
    overflow: hidden;
}
.mySubContainer1 {
    display: flex;
    flex-direction: column;
    width: 100px;
}
.myBox2 {
    width: 100px;
    height: 30px;
    background-color: #ffff00;
}
.mySubSubContainer1 {
    width: 50px;
    /* 親がフレックスボックス(flex-direction: column)なので flex-grow: 1 での引き伸ばしで高さが決まる */
    flex-grow: 1;
    container-type: size;
}
.myBox3 {
    width: 100cqh;
    height: 100cqh;
    background-color: #a0a0ff;
    overflow: hidden;
}
<div class="myContainer1">
    <div class="myBox1">1:縦に拡縮可能ボックス</div>
    <div class="mySubContainer1">
        <div class="myBox2">2:固定サイズ</div>
        <div class="mySubSubContainer1">
            <div class="myBox3">3:緑ボックスの拡縮に合わせてサイズ変動</div>
        </div>
    </div>
</div>

※ お使いのブラウザーでは正しく表示されません。

1:縦に拡縮可能ボックス
2:固定サイズ
3:緑ボックスの拡縮に合わせてサイズ変動

「長さ」を「数値」に変換

※ このセクションの参考: Typecasting and Viewport Transitions in CSS With tan(atan2()) | CSS-Tricks

cqw」「cqh」は長さの単位であるため、純粋な数値(スカラー値)が求められるスタイル、例えば「scale」や「line-clamp」などにはそのままでは使用することはできません(これは cqw cqh に限らず、その他の単位でも同様です)。

そこで、tanatan2 という関数を用いると、「長さ」同士の割り算を数値に変換することができます。これは、atan2 の2つの引数が「dimension」(「長さ」は「dimension」の一部)を受け付け、かつ異なる単位も許容されているためです。

※ 数式上は x / y = tan(atan2(x, y)) になります。三角関数の詳細については省略します。

これを利用すると、本来単位無しの数値が必要な箇所においても、単位付きの値を適用することができるようになります。

以下の例では、親ボックスの幅と高さを純粋な数値に変換して「counter」値に代入し、content で使用しています。緑色ボックスのサイズを変更するとテキストが合わせて変化することが確認できます。

@supports (container-type: size) and (counter-reset: w tan(atan2(var(--myLength1), 1px))) {
    .cssSample2NotSupported {
        display: none;
    }
}
@property --myLength1 {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}
@property --myLength2 {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}
.myContainer2 {
    width: 210px;
    height: 120px;
    box-sizing: border-box;
    resize: both;
    background-color: #00ff00;
    overflow: hidden;
}
/* cqw / cqh の対象コンテナー */
.myBox5 {
    width: 100%;
    height: 100%;
    container-type: size;
}
.myBox5::before {
    /* うまく計算させるためにいったん「長さ」の変数に代入する必要がある */
    --myLength1: 100cqw;
    --myLength2: 100cqh;
    counter-reset: w tan(atan2(var(--myLength1), 1px)) h tan(atan2(var(--myLength2), 1px));
    content: "w: " counter(w) ", h: " counter(h);
}
<div class="myContainer2">
    <div class="myBox5"></div>
</div>

※ お使いのブラウザーでは正しく表示されません。

応用例1: line-clampの自動計算

line-clamp はテキストが指定行数を超えたときに省略表示にするスタイルであり、「-webkit-」接頭辞付きながら多くのブラウザーでサポートされています。line-clamp に指定すべき値は「数値」であり「長さ」ではないため、ボックスの高さなどから自動計算するには従来はスクリプトの力を借りる必要がありました。

しかし、上記の cqh と「長さ → 数値 の変換」を用いると、これをCSSのみで計算して適用することができるようになります。

※ 似たようなスタイルに「text-overflow」がありますが、これは複数行の省略に対応していません。

以下がその例で、桃色ボックスのサイズに合わせたテキストの省略表示が行われることが確認できると思います。

※ お使いのブラウザーでは正しく表示されません。

いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす

上記のソースコードは以下です。なお、途中に出てくる「lh」は「1行の高さ」を表し、他のスタイルを含めて計算された「line-height」のピクセル数に変換できます。

/* 「長さ」を逐次計算させるための変数宣言 */
@property --myLength3 {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}
@property --myLength4 {
    syntax: "<length>";
    inherits: false;
    initial-value: 0px;
}
/* ここでは「line-clamp」「cqh」「lh」「round」を利用 */
@supports ((line-clamp: 1) or (-webkit-line-clamp: 1)) and (height: 100cqh) and (height: 1lh) and (line-height: round(down, 1.1)) {
    .cssSample3NotSupported {
        display: none;
    }
}
.myContainer3 {
    width: 100px;
    height: 100px;
    resize: both;
    overflow: hidden;
    background-color: #ffa0e0;
}
/* cqh の対象コンテナー */
.myBox7 {
    width: 100%;
    height: 100%;
    container-type: size;
}
.myBox8 {
    /* px に変換するために変数に代入 */
    --myLength3: 100cqh;
    --myLength4: 1lh;
    /* 「ボックスの高さ÷1行の高さ」の小数部分切り捨てにより、ボックスに収まる行数を計算 */
    --myLines: round(down, tan(atan2(var(--myLength3), var(--myLength4))));
    /* ボックスに収まる行数からボックスの高さを逆計算 */
    height: calc(var(--myLength4) * var(--myLines));
    /* 「--myLines」を使った line-clamp 指定 */
    display: -webkit-box;
    line-clamp: var(--myLines);
    -webkit-line-clamp: var(--myLines);
    -webkit-box-orient: vertical;
    overflow: hidden;
}
<div class="myContainer3">
    <div class="myBox7">
        <div class="myBox8">いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす</div>
    </div>
</div>

応用例2: テキストがボックスに収まるように横に縮小する

transform: scaleX()」もまた「長さ」ではなく「数値」が必要なスタイルですが、ここまでの手法を同様に適用すれば、横方向の拡縮も行えるようになります。

以下の例では、テキストがボックスに収まらない場合は横方向の縮小を行いますが、サイズが広がって収まるようになった場合は拡縮を行わないスタイルになっています。

※ お使いのブラウザーでは正しく表示されません。

The quick brown fox jumps on the lazy dog

The quick brown fox jumps on the lazy dog

上記のソースコードは以下です。「テキストの実際の幅」を得るために、同じテキストを2回記述して計算させており、ダミーの方には念のため「role="none"」を設定しています。

@property --myLength5 {
    syntax: "<length>";
    inherits: true;
    initial-value: 0px;
}
@property --myLength6 {
    syntax: "<length>";
    inherits: true;
    initial-value: 0px;
}

@supports (display: grid) and (transform: scaleX(min(1, tan(atan2(var(--myLength5), var(--myLength6)))))) and (height: 100cqh) {
    .cssSample4NotSupported {
        display: none;
    }
}

.myContainer4 {
    width: 200px;
    padding: 16px;
    overflow: hidden;
    resize: horizontal;
}

/* テキストを表示するボックス(テキストをこのボックスに収める) */
.myBox11 {
    width: 100%;
    /* 1行テキストなので 1lh を利用 */
    height: 1lh;
    container-type: size;
    background-color: #ffe0a0;
    position: relative;
    /* グリッドレイアウトを用い、 .myBox12 と .myParagraph2 の幅を大きい方に揃える */
    display: grid;
    grid-template-rows: 1fr;
}

.myBox12 {
    /* .myBox11 の幅を取得 */
    --myLength5: 100cqw;
    /* さらに container-type を指定して新たなコンテナーを作る */
    container-type: size;
    /* .myParagraph1 を .myBox12 に合わせるための調整 */
    position: relative;
    /* サイズ自体は .myBox11 に合わせる(実際にはグリッドレイアウトにより調整される) */
    width: 100%;
    height: 100%;
}

.myParagraph1 {
    /* .myBox12 の幅を取得(グリッドレイアウトにより .myParagraph2 の幅になる) */
    --myLength6: 100cqw;
    padding: 0;
    margin: 0;
    /* .myBox12 とぴったり同じボックスにする */
    position: absolute;
    inset: 0;
    /* 拡縮の起点を左端にする(text-align で中央揃えにする場合は center を用いるなどで調整できます) */
    transform-origin: left;
    /* 「--myLength5 / --myLength6」で縮小率を計算、ただし 1 を超える場合は 1 を設定 */
    transform: scaleX(min(1, tan(atan2(var(--myLength5), var(--myLength6)))));
}

/* テキスト幅を計算するためだけのダミーボックス */
.myParagraph2 {
    visibility: hidden;
    /* 高さはいらないので念のため 0 を指定 */
    height: 0;
    padding: 0;
    margin: 0;
    white-space: nowrap;
}
<div class="myContainer4">
    <div class="myBox11">
        <div class="myBox12">
            <p class="myParagraph1">The quick brown fox jumps on the lazy dog</p>
        </div>
        <p class="myParagraph2" role="none">The quick brown fox jumps on the lazy dog</p>
    </div>
</div>

応用例3: ボックス内のレイアウトを維持しながら自動拡縮

あるボックス(コンテナー)内に画像やテキストなどを配置した後、そのボックス自体を親要素に応じて自動的に拡縮(あるいはサイズが異なる親要素に合わせて自動フィット)させる場合にも、cqw および cqh を使えばCSSのみで拡縮を計算させることができます。これは、特にボックスをあたかも1つの要素(疑似画像またはSVG)のように扱って再利用したい場合に強力に効果を発揮します。

下の例では、「- 見出し -」のあるボックスが2つ配置されており、HTML、およびスタイル(class)は同一ですが、ボックス全体のサイズは異なっています。しかしながら、異なるサイズでもボックス内の配置は保たれています。

※ お使いのブラウザーでは正しく表示されません。

- 見出し -

- 見出し -

上記のソースコードは以下です。親要素(コンテナー)のサイズを得るために .myBox15 > .mySubBox15-1 という構造にしており、.mySubBox15-1 内で transform: scale(...) を使うことでサイズ調整を行っています。その .mySubBox15-1 自身は「width: 600px; height: 400px」という固定サイズでレイアウトを組んでおり、その中の要素も同様に固定サイズを使っています。これにより、親要素のサイズにとらわれずにレイアウトを組むことが可能になっています。

@property --myLength7 {
    syntax: "<length>";
    inherits: true;
    initial-value: 0px;
}

@property --myLength8 {
    syntax: "<length>";
    inherits: true;
    initial-value: 0px;
}

@supports (transform: scaleX(min(1, tan(atan2(var(--myLength7), var(--myLength8)))))) and (height: 100cqh) {
    .cssSample5NotSupported {
        display: none;
    }
}

/* サイズの提供元となるコンテナー1 */
.myContainer5 {
    padding: 16px;
    overflow: hidden;
    resize: both;
    border: solid 1px #ccc;
    background-color: #ffffc0;
    width: 300px;
    height: 200px;
}

/* サイズの提供元となるコンテナー2 */
.myContainer6 {
    padding: 16px;
    overflow: hidden;
    resize: both;
    border: solid 1px #ccc;
    background-color: #ffffc0;
    width: 150px;
    height: 100px;
}

/* 親コンテナーのサイズを得るためのボックス(兼レイアウトの実際のルート要素) */
.myBox15 {
    container-type: size;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    background-color: #c0ffc0;
}

/* サイズが絶対値となっているレイアウトのルート要素 */
.mySubBox15-1 {
    /* 親コンテナーのサイズを取得 */
    --myLength7: 100cqw;
    --myLength8: 100cqh;
    /* このボックスのサイズを定義 */
    width: 600px;
    height: 400px;
    box-sizing: border-box;
    /* 親コンテナーのサイズとこのボックスのサイズから拡大率を計算 */
    /* ※ 一旦 --myLength7 と --myLength8 の変数に展開しないと正しく計算できない */
    transform: scale(tan(atan2(var(--myLength7), 600px)), tan(atan2(var(--myLength8), 400px)));
    /* 拡縮の基準点を左上に置くことであたかも親要素のサイズに合わせているように見せる */
    transform-origin: top left;

    /* 以下はボックス内のレイアウトを組むためのスタイル */
    margin: 0;
    padding: 16px;
    border: 1px solid #000;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
}

/* -header, -content, -footer はボックス内のレイアウトを組むための適当なスタイル */
/* 各種サイズ値はボックスのサイズ(この場合は 600px×400px)の前提で決めることができる */
.mySubBox15-1-header {
    margin: 0;
    padding: 0;
    text-align: center;
    font-size: 24px;
    line-height: 36px;
}

.mySubBox15-1-content {
    margin: 0;
    padding: 0;
    flex-grow: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.mySubBox15-1-content-button {
    flex: 0 0 1;
    width: 32px;
    height: 32px;
    font-size: 20px;
}

.mySubBox15-1-content-image {
    width: auto;
    height: 260px;
}

.mySubBox15-1-footer {
    margin: 0;
    padding: 0;
    text-align: right;
    font-size: 20px;
    line-height: 30px;
}
<!-- .myContainer5 と .myContainer6 の中のHTMLは同一(Reactなどであれば同一コンポーネントにできる) -->
<div class="myContainer5">
    <div class="myBox15">
        <div class="mySubBox15-1">
            <p class="mySubBox15-1-header">- 見出し -</p>
            <div class="mySubBox15-1-content">
                <button class="mySubBox15-1-content-button" role="img">A</button>
                <img src="cqw_cqh_img1.jpg" width="983" height="1306" class="mySubBox15-1-content-image" />
                <button class="mySubBox15-1-content-button" role="img">B</button>
            </div>
            <p class="mySubBox15-1-footer">フッター</p>
        </div>
    </div>
</div>
<div class="myContainer6">
    <div class="myBox15">
        <div class="mySubBox15-1">
            <p class="mySubBox15-1-header">- 見出し -</p>
            <div class="mySubBox15-1-content">
                <button class="mySubBox15-1-content-button" role="img">A</button>
                <img src="cqw_cqh_img1.jpg" width="983" height="1306" class="mySubBox15-1-content-image" />
                <button class="mySubBox15-1-content-button" role="img">B</button>
            </div>
            <p class="mySubBox15-1-footer">フッター</p>
        </div>
    </div>
</div>