こなさんみんばんわ。

CSS グリッド・レイアウトの repeat() 関数で使えるキーワード値 auto-fillauto-fit の挙動の違いを確認するために、画像サムネイルのリストを display: grid で格子状に並べたものを作りまして、最初は開発者ツールを使って手入力でグリッド幅を変更してたんですが😅
そのうち「これ幅をカスタム・プロパティにして JavaScript でコントロールしたら、開発者ツール開かんでもページ上で変更できるんと違うん?」ということに気が付きました。

ちょうど JavaScript でカスタム・プロパティの値を取得したり設定したりする方法を(今までやったことなくて)知りたかったというのもあり、その辺も調べつつがんばってみましたら、そこそこ問題なく動くものができ上がりましたので、本日はその一連の作業をステップ by ステップ形式のチュートリアルっぽくまとめてみようと思います。

なお、今回各ステップごとにリンクしているサンプルは GitHub Pages にまとめて公開しておりますtarget 属性を付けて、あえて別タブで開くようにしております(開くタブはひとつだけで、追加で何枚もは開きません)ので、そちらを確認しながらお読みいただけますと幸いです。

作るもの

スライダーで拡大縮小ができる画像一覧のデモ動画 ※クリック or タップでループ再生開始・音声なし

アニメ GIF でいいものを無駄に動画でやってみました…いや、こんな機会でもないと一生 video 要素なんて使わんのやないかと思いまして。涙

そんな訳で、動画のようにスライダーを動かすとサムネイル幅が大きくなったり小さくなったりする画像一覧を作っていきます。あくまでもデモなので、ところどころ手抜きだったり適当だったりする部分はあります1が、あまり細かい部分は気にしないでいただけますとありがたいです。

まずは基本の HTML

画像が並んだリストを HTML で作る

とりあえずコンテンツがないと始まらないので、まずは HTML を書きましょう。画像をたくさん準備するのはめんd…大変なので、今回は Unsplash の画像をプレースホルダー画像として使える無料の Web サービス、Lorem Picsum をありがたく使用させていただくこととします。

HTML コードは以下のようになります。画像幅は 240 ピクセル四方ですが、Retina ディスプレイを想定して実際に取得する画像は 480 ピクセル四方としています。それ以外、特に説明は不要ですよね…

<ul>
  <li><img alt="サンプル画像 1" src="https://picsum.photos/480?random=1" height="240" width="240"></li>
  <li><img alt="サンプル画像 2" src="https://picsum.photos/480?random=2" height="240" width="240"></li>
  <!--
    ほぼ同じコードが延々と続くので省略
  -->
  <li><img alt="サンプル画像 20" src="https://picsum.photos/480?random=20" height="240" width="240"></li>
</ul>

画像一覧っぽくスタイルを整える

CSS グリッド・レイアウトで画像を格子状に並べる

コンテンツができたので、次にベースとなるスタイル付けをしていきます。冒頭で書いたとおり、repeat() 関数で使えるキーワード値 auto-fillauto-fit の挙動の違いを確認するというのが主目的なので、当然 CSS グリッドでレイアウトします。

ul {
  display: grid;
  gap: 2px;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  padding: 0;
  list-style: none;
}
img {
  max-width: 100%;
  width: auto;
  height: auto;
  vertical-align: bottom;
}

grid-template-columns: repeat() の行だけ説明しておくと、minmax() という関数は CSS グリッドのトラック幅や高さを○○以上○○以下の範囲に収めてくださいと指定するものです。また繰り返し回数に使われる auto-fillauto-fit といったキーワードは、グリッド・コンテナの幅や高さに合わせていい感じに回数を決めてくださいと指示するものです2。あと念のために 1fr というのはフレックス係数の指定で、余ったスペースをその割合に従って分割してくださいという意味です。

なので、上のコードを言葉で説明すると「各アイテムの幅 100px + 隙間 2px でコンテナ内を埋め尽くせるだけ埋め尽くしてみて、それでちょうど隙間なく収まるなら 100px でいいし、余りが出るようならその余りをアイテム数で等しく分割して 100px にプラスしたものをアイテム幅として設定してください」みたいな感じでしょうかね。

その他の部分(img 要素のスタイルとか)は説明不要かと思いますので省略します。

拡大縮小機能のための下準備

range 型の input 要素で幅設定用のスライダーを実装、他

以上で基本的な構造ができ上がりました。ここからは拡大縮小機能を付け加える作業をしていきましょう。まずは下準備から。

グリッド最小幅をカスタム・プロパティに置き換える

今回、画像の幅はグリッド・アイテムの幅で制限されるように CSS を書いているので、この幅を制御している部分、つまり minmax() 関数の最小値をカスタム・プロパティで置き換えましょう。先ほど書いた CSS に次のような追加・変更を加えます。

:root {
  --grid-min-size: 100px;
}
ul {
  /* 省略 */
  grid-template-columns: repeat(auto-fill, minmax(var(--grid-min-size, 100px), 1fr));
  /* 省略 */
}

:root セレクタで html 要素に --grid-min-size というプロパティを定義して、これを先ほどの minmax() 関数内の固定値部分 (100px) と置き換えて使います。念のために var(--grid-min-size, 100px) としてフォールバック値を与えております。

グリッド最小幅を変更するためのスライダーを付ける

次に画像幅を変更するためのスライダーを付けましょう。input 要素を type="range" とするとスライダー型の UI が提供されます3ので、これを使います。あとこれは個人的な勉強用として、スライダーの値を視覚的にフィードバックするための output 要素を付けてみました。あとでスクリプトから値を取得して設定しますが、とりあえず初期値の 100px をベタ書きで入れておきます。

<form>
  <label>画像の最小幅:
    <input type="range" min="20" max="240" value="100">
  </label>
  <output>100px</output>
</form>

これだけだとスライダーの位置が上付き気味になってちょっと気になりましたので、CSSvertical-align: middle としておきました。

input[type="range"] {
  vertical-align: middle;
}

JavaScript で操作などをする要素に id を振る

最後に、JavaScript で操作したい要素に id を振っていきます。スライダーの input 要素とその値を表示する output 要素、あとは変更されたプロパティ値をセットする対象となる、グリッド・コンテナの ul 要素ですね。

<form>
  <label>画像の最小幅:
    <input type="range" min="20" max="240" value="100" id="slider">
  </label>
  <output id="monitor">100px</output>
</form>
<ul id="thumbnails">
<!-- 以下省略 -->

ここで「:root でカスタム・プロパティを設定してるんだから変更対象も :root でいいんと違うの?」と思われる方がいるかもしれませんが、基本的に :root で設定するカスタム・プロパティはグローバル値や初期値として扱うことになるかと思いますので、これを直接変更してしまうとあとで他のスクリプトがこの値を参照したいとなった時にマズイのですよね。

また CSS カスタム・プロパティには継承の仕組みがありますので、:root のプロパティ値を変更してしまうと、それが継承されて意図しない要素にまで適用されてしまうこともあり得ます。

まぁ今回は他のスクリプトなどはないですし、カスタム・プロパティを使ってる部分も少ないので、気にしなくても大して影響はないのですが、とりあえずそういった事情がある以上、カスタム・プロパティ値を変更する際はなるべく局所的に適用されるよう、普段から意識付けをしておいた方がいいかなと思っております。

拡大縮小機能を実装していく

スライダーでグリッド幅を広げたり狭めたりできるようにする

そんなこんなで下準備も終わりましたので、ここからはいよいよ拡大縮小機能を実現するためのコードを JavaScript で書いていきましょう。

とりあえず最低限の動くコード

細かい部分はあとで調整するとして、まずはとりあえず必要な処理が動く最低限のコードを書いてみます。

// 前準備
const propName = "--grid-min-size";
const slider = document.getElementById("slider");
const monitor = document.getElementById("monitor");
const thumbnails = document.getElementById("thumbnails");

// メイン処理
slider.addEventListener("input", (e) => {
  const propValue = `${e.target.value}px`;
  thumbnails.style.setProperty(propName, propValue);
  monitor.textContent = propValue;
});

前半は設定するカスタム・プロパティ名を変数に入れてるのと id 振った要素を取得しているだけなので、特に説明は必要ないですね。

後半は実際にカスタム・プロパティの値を変更するコードとなりますが、まずポイントとして要素にカスタム・プロパティを設定するには CSSStyleDeclaration オブジェクトの setProperty() メソッドを使います。えっ普通に color とか設定する時と同じですやん…そりゃそうです、カスタム・プロパティだって普通の CSS プロパティとルールの書き方は一緒ですからね。気が付けば簡単な話なのでした😅4

// 変数 p には特定の p 要素が格納されているとする
// これは <p style="color: red"> とやってるのと一緒
p.style.setProperty("color", "red");
// これは <p style="--text-color: red"> とやってるのと一緒
p.style.setProperty("--text-color", "red");
// 要するに、どちらもまとめてこう表現できる
p.style.setProperty("プロパティ名", "プロパティ値");

という訳で、このメイン処理の部分でやっている内容は、

  1. スライダーが操作されたら (input イベントが発生したら)
  2. 現在のスライダーの value プロパティを拾ってカスタム・プロパティの値(○○px)を作成
  3. その値をグリッド・コンテナである ul 要素にカスタム・プロパティ --grid-min-size として与え
  4. かつフィードバック用の output 要素のテキスト内容としても設定する

という流れになっています。

以上、ここまでやったところで、思ったことを実現するコードは一応完成しているように思います。次のサンプルで実際に試してみてください。

動かしてみて気になる部分の手直し

さて、とりあえず動くコードは実装できましたが、実際に色々な端末で開いて操作してみると、若干気になる部分が出てきました。ここからはその気になる部分を修正していきたいと思います。

狭い画面ではグリッドの最小幅を半分にする

狭い画面ではもう少し小さな画像サイズでもよいように思う

画像サイズの初期値は現在、環境に関わらず 100px としています。PC 画面はこれで問題ないですが、スマホのような狭い画面では少し大きいように思えます。あと、増える方向にもっと変化の段階があってもいいですよね。

そんな訳で、カスタム・プロパティを定義しているコードを次のように変更してみました。

:root {
  --large-image-size: 100px;
  --small-image-size: 50px;
  --grid-min-size: var(--large-image-size);
  @media (width < 640px) {
    --grid-min-size: var(--small-image-size);
  }
}

まずは --large-image-size--small-image-size という 2 つの新しいカスタム・プロパティを設定しています。これはカスタム・プロパティ値に別のカスタム・プロパティ値を指定できるという見本としても意味合いもありますが、こうやって別の変数として分けておけば、今後コードが増えた場合に別の箇所でもこの 2 つが使える場面があるかもしれないよね…というのも少し想定しています。

それからメディアクエリーを使って、デフォルトでは --large-image-size が、幅 640px 未満の画面では --small-image-size が、--grid-min-size の値として設定されるようにしています。このように閲覧環境に応じてダイナミックに値の変更ができるのは CSS カスタム・プロパティが Sass などの変数と決定的に違う部分であり、メリットでもありますね。この利点は積極的に使っていきたいところです。

スライダー位置や表示の初期化処理を入れる

スライダー数値と実際の画像サイズが一致してないので合わせる処理を入れましょう

こうして狭い画面では画像幅が初期状態では 50px で表示されるようになりましたが、そうなると次は、決め打ちでベタ書きしていたスライダーの value 属性値やフィードバック表示が実際の画像幅と合わないという状況が発生します。

また、少しスライダーを動かしただけで急に画像幅が 100px 近くに大きくなるので、触った人はびっくりしてしまうかもしれません😅

そんな訳で、これを解消するための初期化処理を入れようと思います。ここまでのコードの前準備とメイン処理の間に、次のようなコードを差し込んでみましょう。

// 初期化処理
const root = document.documentElement;
const styles = getComputedStyle(root);
const initial = styles.getPropertyValue(propName);
const integer = parseInt(initial);
slider.value = integer;
monitor.textContent = initial;

まずは各カスタム・プロパティの初期値が設定されている :root に当たる要素(HTML ページなのでここではもちろん html 要素です)を取得します。次にこの要素に設定されているカスタム・プロパティ --grid-min-size の値を取得します。「あーじゃあこれも設定する時と同じく CSSStyleDeclaration オブジェクトの、今度は getPropertyValue() メソッドを使用すればいいのだな」と思いましたか? その通りです! ってもうコードに書いてあるから分かるわな🤣

ただしこちらは root.style.getPropertyValue() では取れないので(だって style 属性付いてないものね…)まずは getComputedStyles(root) で全スタイルの入った CSSStyleDeclaration オブジェクトを取得した上で、そのオブジェクトを通して getPropertyValue() で読み取る必要があります。この辺は状況に応じて適切に判断しないといけないです。

あとは取得したプロパティ値を使って、input 要素の value プロパティと output 要素内のテキストを設定してあげれば完了です。取得できる値は px 単位付きなので value プロパティ値にするためには単位を取り除いて整数にしないといけませんが、今回は雑に parseInt() でやりました…本当はもうちょっとちゃんとした方がいいんだとは思います。涙

狭い画面でのスライダー可動範囲も調整する

上方向へのスライダーの余裕がありすぎて無駄かも…という訳で調整しましょう

初期化処理を入れてスライダー位置が実際の画像幅と一致するようになったぞ、これでめでたしめでたし…といきたかったのですが、実際にここまでのサンプルをスマホで操作してみると、

  • 縮小方向へはまだ 10 段階くらい縮まるのにスライダーの余裕がない
  • 逆に拡大方向へは 6 段階くらいしか大きくならないのにスライダーの余裕があり余っている

というのが、気になるといえば気になりますかね。画像のように 90px にした時点であと 1 つか 2 つしか縮まらないのに、まだ 2/3 くらいスライダーに余裕があるのは、ちょっと無駄に思えます。

という訳で、狭い画面の時にはこのスライダーの最大値も半分くらいにしましょう。アクセスしてきたデバイスがメディアクエリー (width < 640px) に合致するかを調べて、合致する場合にはスライダーの max 値を 120 に変更するコードを、先ほどの初期化処理の後に追加します。

// スライダー範囲の調整
if (window.matchMedia("(width < 640px)").matches) {
  slider.max = 120;
}

はい、これで気になる部分にも手を入れて、ひととおり満足できるものができ上がりました! ワーパチパチ
完成したサンプルを置いておきますので、色々なデバイスや画面幅でご覧になり、操作してみていただければと思います。

学んだこと(まとめ)

  • 要素にカスタム・プロパティを設定・変更するには element.style.setProperty("--property-name", propertyValue)
  • 要素のカスタム・プロパティ値を取得するには getComputedStyle(element).getProperty("--property-name")
  • これらはどちらも普通の CSS プロパティを取得・設定・変更する方法と一緒であり、カスタム・プロパティだからといって特に難しい作業が発生する訳ではない

こんなところですかね。何となく「きっと今までとは違う特殊な作業が必要なんだろうな」とか思ってたんですが違うんですね、考えすぎでした😭

そんな訳で

今回は、スライダーで拡大縮小ができる画像一覧リストを作りながら JavaScript を使って CSS カスタム・プロパティを取得したり操作する方法を調べて覚えたので、その方法も含めて一連の手順をステップ by ステップ形式のチュートリアルっぽくまとめてみましたよ、といった趣の内容でした。

本文中にも書いたように「気が付けば簡単な話」ではあるのですが、ただそれも実際に作業してみたことで + こうやってブログに書くことで、はじめて「なーんや、簡単な話やんけ」ということに気が付けた訳ですから、やっぱり自分で手を動かしてみることは大切だよなぁ…と、あらためて感じますね。

ちなみにコードを丸ごと利用していただくのもいいですが5、どちらかというと今回は実装のプロセスであるとか、そこに至るまでの経緯や考え方といった部分に主眼を置いて書いてみたところもありますので、そういった点でも何らかの参考になりましたら幸いでございます。

以上、こんな長文をここまでお読みいただき、ありがとうございました😭


…で終わろうとしたんですが、今回のサンプルには当初の主目的からすると完全に欠けてる部分がありまして。ええ、お気付きだとは思いますが、auto-fillauto-fit を切り替える機能ですね…はい、後日それを追加する記事を書きます。

乞うご期待! 涙

  1. class を割り振らずに直接要素型セレクタでスタイル付けしてるとか、parseInt() で雑にテキストから整数に変換してるとか、あたりです。 

  2. ここは今回のメイン内容ではないので、くわしくは別の機会に説明したいと思います。とりあえず今日の時点ではこの 2 つには、コンテナ内の一行(や一列)にすべてのアイテムが収まってなお余りがある場合の所作に大きな違いがある、程度の認識で十分です。2 行以上にまたがってる場合はどちらも挙動に違いはありません。 

  3. 「現行の主要な Web ブラウザでは」という但し書きが付きます。もしかするとダイヤルのような UI になるかもしれませんね。もちろん range 型を理解しないブラウザは普通のテキストボックスを表示するでしょう。 

  4. まぁ普通の CSS プロパティについては element.style.property = "value" という構文が準備されているので、この形を使うことはあまりありませんが… 

  5. …と書きましたが、何度もいうように文中のコードは雑なところがけっこうあるので、使う時には手を入れてくださいね。本当に丸ごと使って何かあっても僕は責任取りませんよ。涙