一覧WB-09-3 · CSSレイアウト

レスポンシブデザインと画像の選択

同じ文書の表示幅を640pxと360pxに変更し、列数を確認します。続いて長い一行をvisible・hidden・clip・autoで扱い、内部スクロールの差を調べます。最後にsrcsetとsizesで幅別の画像候補を指定し、選ばれた画像と表示幅を確認します。

説明

表示領域が狭くなると、並び方を変える、内容を折り返す、内部をスクロールさせるなどの対応が必要になります。メディアクエリは表示条件に応じてCSSを切り替え、overflowは箱からはみ出た内容をどう扱うかを決めます。

同じ文書の表示幅を640pxと360pxに変更し、列数を確認します。続いて長い一行をvisible・hidden・clip・autoで扱い、内部スクロールの差を調べます。最後にsrcsetとsizesで幅別の画像候補を指定し、選ばれた画像と表示幅を確認します。

1幅に応じて列数を変える

広い画面は二列、480px以下は一列になるCSSを用意します。

(globalThis.doc=createFormDocument('<!doctype html><html lang="ja"><head><style>body{font:16px/24px system-ui;margin:16px;background:#111;color:#eee}#cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}#cards>div{background:#22434b;padding:8px}@media(max-width:480px)狭い表示領域の条件{#cards{grid-template-columns:1fr}}</style></head><body><h2>資料一覧</h2><div id="cards"><div id="cardA">HTML</div><div id="cardB">CSS</div></div></body></html>'),{viewport:doc.defaultView.innerWidth,small:doc.defaultView.matchMedia同じ条件の一致を確認("(max-width:480px)").matches,a:rect("#cardA"),b:rect("#cardB")})

表示領域640pxでは条件がfalseで、カードは同じ行の二列です。メディアクエリはCSSを適用する条件であり、HTMLの要素を別ページへ移す機能ではありません。

2360px幅へ変更

文書の表示領域を360pxへ縮めます。

(setViewport(360)文書の表示領域を360pxへ,{viewport:doc.defaultView.innerWidth,small:doc.defaultView.matchMedia("(max-width:480px)").matches条件が成立するか取得,a:rect("#cardA"),b:rect("#cardB")})

条件がtrueになり、BがAの下へ移ります。DOMの順序は変えずに、CSSの列数で並び方を変えています。端末名から推測するのではなく、実際の表示幅で条件を確認します。

3同じ幅でも文字が大きくなる場合

bodyの文字を32pxへ拡大します。

(doc.body.style.fontSize="32px"文字サイズを拡大,doc.body.style.lineHeight="1.5"文字サイズに応じた行高,{viewport:doc.defaultView.innerWidth,small:doc.defaultView.matchMedia("(max-width:480px)").matches,a:rect("#cardA")})

表示幅とメディア条件は同じでも、文字やカードの高さが増えます。幅だけを見て固定高さを決めると、文字サイズが変わったときに内容が収まらない場合があります。

4visibleでは箱の外へ描かれる

文字サイズを戻し、幅200pxの箱へ長い一行を追加します。

(doc.body.style.fontSize="16px",doc.body.style.lineHeight="24px",doc.body.insertAdjacentHTML("beforeend",'<div id="overflow" style="width:200px;white-space:nowrap幅を制限し改行しない;font-family:monospace;background:#57304a;overflow:visible外側の内容も描画"></div>'),globalThis.overflowBox=doc.getElementById("overflow"),overflowBox.textContent="0123456789".repeat(5),{client:overflowBox.clientWidth,scroll:overflowBox.scrollWidth内容の幅,mode:styleOf("#overflow","overflow-x")})

内容幅scrollWidthは表示幅clientWidthより大きく、visibleでは文字が箱の外へ描かれます。箱の幅200pxが、文字の描画範囲まで自動的に200pxへ切り取るわけではありません。

5hiddenでもプログラムからスクロールできる

overflow:hiddenにし、scrollLeftを60へ設定します。

(overflowBox.style.overflow="hidden"外側を切り取る,overflowBox.scrollLeft=60内部の表示位置を変更,{left:round2(overflowBox.scrollLeft),client:overflowBox.clientWidth,scroll:overflowBox.scrollWidth})

外側の内容は切り取られますが、scrollLeftは約60になり、内部の表示位置を変更できます。hiddenはスクロールバーを表示しないものの、プログラムからのスクロールは可能です。

6clipではスクロール領域を作らない

clipへ変え、もう一度60へ移動しようとします。

(overflowBox.style.overflow="clip"スクロール領域を作らず切り取る,overflowBox.scrollLeft=60移動できるか確認,{left:round2(overflowBox.scrollLeft),mode:styleOf("#overflow","overflow-x")})

scrollLeftは0です。clipは内容を切り取りますが、スクロールコンテナを作らず、プログラムからのスクロールにも使えません。hiddenと見た目が似ていても動作は異なります。

7autoで内部スクロール

autoへ変え、表示位置を60にします。

(overflowBox.style.overflow="auto"必要な場合にスクロール,overflowBox.scrollLeft=60内部を移動,{left:round2(overflowBox.scrollLeft),client:overflowBox.clientWidth,scroll:overflowBox.scrollWidth})

内容がはみ出しているためスクロールでき、約60へ移ります。スクロールバーの常時表示か自動表示かは環境にもよりますが、内容幅と表示幅、scrollLeftで状態を確認できます。

8内容を折り返してあふれを減らす

white-spaceをnormalへ戻し、長い語の改行も許可します。

(overflowBox.style.whiteSpace="normal"通常の改行処理,overflowBox.style.overflowWrap="anywhere"長い語にも改行を許可,overflowBox.scrollLeft=0,{height:rect("#overflow").height,client:overflowBox.clientWidth,scroll:overflowBox.scrollWidth})

文字が幅200pxの中で折り返され、高さが増えます。横幅を広げずに内容を収める代わりに、縦方向の領域が必要になります。

9幅の異なる画像候補

320・640・1280・2560pxの候補を作り、現在の表示幅に合わせて選ばせます。

imageReadyは表示更新を待ってからimage.decodeで画像の復号完了を待つ関数です。表示幅を変えた直後の、更新前の選択値を取得しないようにします。

(globalThis.candidates=[320,640,1280,2560]画像自体の幅の候補.map(width=>({width,url:picture(width)})),globalThis.image=doc.createElement("img"),image.id="responsive",image.alt="選択された画像のピクセル幅",image.style.width="100%",image.src=candidates[0].url,image.srcset=幅記述子付きの候補一覧candidates.map(c=>c.url+" "+c.width+"w").join(", "),image.sizes="calc(100vw - 32px)"想定する表示幅,doc.body.append(image),await imageReady()選択画像の復号完了を待つ,imageInfo())

srcsetのwは各候補の画像幅、sizesはCSS上で想定する表示幅です。この文書は左右に16pxずつ余白があるため、表示領域から32pxを引いています。ブラウザは表示幅と画素密度などを考慮して候補を選び、currentSrcで実際の選択を確認できます。

10表示幅を広げて選択を確認

文書を640px幅へ戻してから画像の状態を確認します。

(setViewport(640)表示領域を広げる,await imageReady(),imageInfo()表示幅・密度・選択候補)

画像のCSS表示幅は約608pxになります。必要な画像の画素数はCSS幅だけではなくdensityの値にも関わります。この例では1280pxから2560pxの候補へ切り替わります。candidateは画像自体の幅であり、画面上の表示幅と同じ数になるとは限りません。

11狭い幅へ戻したときの保持

360px幅へ戻し、もう一度選択を取得します。

(setViewport(360)表示領域を縮める,await imageReady()現在の画像を確認して取得,imageInfo())

表示幅は狭くなりますが、ブラウザはすでに取得した大きい画像を保持する場合があります。srcsetは候補を提供する仕組みで、画面幅が変わるたびに必ず最小候補へ切り替える命令ではありません。表示幅・画素密度・実際のcurrentSrcを合わせて判断します。

まとめ

メディアクエリは表示条件に応じてCSSを切り替えます。文字の拡大による高さの変化も確認します。

visible・hidden・clip・autoでは、切取りと内部スクロールの可否が異なります。

srcsetとsizesは画像候補と表示幅を伝え、実際の選択には画素密度や取得済み画像も関わります。

操作履歴と解説を振り返れます。

ブラウザ / HTML・DOM

1
(globalThis.doc=createFormDocument('<!doctype html><html lang="ja"><head><style>body{font:16px/24px system-ui;margin:16px;background:#111;color:#eee}#cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}#cards>div{background:#22434b;padding:8px}@media(max-width:480px){#cards{grid-template-columns:1fr}}</style></head><body><h2>資料一覧</h2><div id="cards"><div id="cardA">HTML</div><div id="cardB">CSS</div></div></body></html>'),{viewport:doc.defaultView.innerWidth,small:doc.defaultView.matchMedia("(max-width:480px)").matches,a:rect("#cardA"),b:rect("#cardB")})
{
  "viewport": 640,
  "small": false,
  "a": {
    "x": 16,
    "y": 63.83,
    "width": 299.97,
    "height": 39.99
  },
  "b": {
    "x": 323.96,
    "y": 63.83,
    "width": 299.97,
    "height": 39.99
  }
}
2
(setViewport(360),{viewport:doc.defaultView.innerWidth,small:doc.defaultView.matchMedia("(max-width:480px)").matches,a:rect("#cardA"),b:rect("#cardB")})
{
  "viewport": 360,
  "small": true,
  "a": {
    "x": 16,
    "y": 63.83,
    "width": 328.08,
    "height": 39.99
  },
  "b": {
    "x": 16,
    "y": 111.82,
    "width": 328.08,
    "height": 39.99
  }
}
3
(doc.body.style.fontSize="32px",doc.body.style.lineHeight="1.5",{viewport:doc.defaultView.innerWidth,small:doc.defaultView.matchMedia("(max-width:480px)").matches,a:rect("#cardA")})
{
  "viewport": 360,
  "small": true,
  "a": {
    "x": 16,
    "y": 151.67,
    "width": 328.08,
    "height": 63.99
  }
}
4
(doc.body.style.fontSize="16px",doc.body.style.lineHeight="24px",doc.body.insertAdjacentHTML("beforeend",'<div id="overflow" style="width:200px;white-space:nowrap;font-family:monospace;background:#57304a;overflow:visible"></div>'),globalThis.overflowBox=doc.getElementById("overflow"),overflowBox.textContent="0123456789".repeat(5),{client:overflowBox.clientWidth,scroll:overflowBox.scrollWidth,mode:styleOf("#overflow","overflow-x")})
{
  "client": 200,
  "scroll": 400,
  "mode": "visible"
}
5
(overflowBox.style.overflow="hidden",overflowBox.scrollLeft=60,{left:round2(overflowBox.scrollLeft),client:overflowBox.clientWidth,scroll:overflowBox.scrollWidth})
{
  "left": 60.17,
  "client": 200,
  "scroll": 400
}
6
(overflowBox.style.overflow="clip",overflowBox.scrollLeft=60,{left:round2(overflowBox.scrollLeft),mode:styleOf("#overflow","overflow-x")})
{
  "left": 0,
  "mode": "clip"
}
7
(overflowBox.style.overflow="auto",overflowBox.scrollLeft=60,{left:round2(overflowBox.scrollLeft),client:overflowBox.clientWidth,scroll:overflowBox.scrollWidth})
{
  "left": 60.17,
  "client": 200,
  "scroll": 400
}
8
(overflowBox.style.whiteSpace="normal",overflowBox.style.overflowWrap="anywhere",overflowBox.scrollLeft=0,{height:rect("#overflow").height,client:overflowBox.clientWidth,scroll:overflowBox.scrollWidth})
{
  "height": 48,
  "client": 200,
  "scroll": 200
}
9
(globalThis.candidates=[320,640,1280,2560].map(width=>({width,url:picture(width)})),globalThis.image=doc.createElement("img"),image.id="responsive",image.alt="選択された画像のピクセル幅",image.style.width="100%",image.src=candidates[0].url,image.srcset=candidates.map(c=>c.url+" "+c.width+"w").join(", "),image.sizes="calc(100vw - 32px)",doc.body.append(image),await imageReady(),imageInfo())
{
  "viewport": 360,
  "displayWidth": 328.08,
  "density": 2.14,
  "candidate": 1280
}
10
(setViewport(640),await imageReady(),imageInfo())
{
  "viewport": 640,
  "displayWidth": 607.93,
  "density": 2.14,
  "candidate": 2560
}
11
(setViewport(360),await imageReady(),imageInfo())
{
  "viewport": 360,
  "displayWidth": 328.08,
  "density": 2.14,
  "candidate": 2560
}

演習完了