追従する目次と、画面の取り合い
スマホで目次を画面上部に貼り付けたい。
PCではすでに右側に追従表示していました。同じことをスマホでもやるだけ——のはずが、実際に置いてみると本文を読む場所が足りなくなりました。貼り付くものは、そこにある間ずっと画面を占め続けるからです。
PCとスマホで同じ機能の置き方が変わる理由、貼り付く要素の高さをどこまで削るか、そして「飛んだ先が隠れる」という副作用への対処。狭い画面での面積の取り合いの話です。
PCとスマホで、余っているものが違う
PCの画面では、本文の横に十分な余白があります。だから目次は右側に置けます。本文の面積を1ピクセルも削らずに、常に見えている状態を作れます。
スマホには横の余白がありません。置けるのは本文の上か下だけです。そして上に貼り付ければ、その高さのぶん本文が押し出されます。
| 余っているもの | 目次の置き場所 | |
|---|---|---|
| PC | 横幅 | 右側に追従(本文は削られない) |
| スマホ | なし | 上に追従(本文が削られる) |
同じ「追従する目次」でも、払っている代償が違います。PCでの成功をそのまま持ち込むと、スマホでは負担のほうが大きくなる可能性があります。
最初の実装は、高すぎた
もともとスマホ版の目次には、進捗バーと「読了まで/あと35%」の表示が縦に積まれ、その下に「目次(8項目)」の開閉ボタンがありました。これをそのまま貼り付けると、こうなります。
ヘッダー 57px
目次バー 85px
────────────────────
合計 142px
iPhone SE の表示領域(667px)に対して 21%
2割が常時埋まります。しかもスマホでは、画面の下端もブラウザのUIに取られています。実際に本文が見えている範囲は、思っているより狭いのです。
縦に積むのをやめる
減らせるものを探しました。表示している情報は3つです。
| 要素 | 役割 | 削れるか |
|---|---|---|
| 「目次」の文字と開閉 | 触れることを伝える | 削れない |
| 進捗バー | どこまで来たかを示す | 削れない |
| 残り%の文字 | 見通しを与える | 削れない |
どれも消せません。消せないなら、並べ方を変えます。縦に積んでいたものを横一列にしました。
[目次 ▸] ──────────── あと 35%
(進捗バー)
これで85pxが約40pxになりました。ヘッダーと合わせて約97px、本文が見える範囲は85%まで戻ります。情報は1つも減らしていません。
置く場所が足りないとき、最初に浮かぶのは「何を消すか」です。しかし消すと機能が減ります。同じ情報を、置ける形に合わせて並べ替えられないかを先に考えると、失わずに済むことがあります。
実装:同じ部品を2通りに並べる
進捗バーの部品には、並べ方を選べるようにしました。
layout?: "stacked" | "row";
// 表示する部品は3つだけ。並べ方が2通りあるので、
// 部品を変数に切り出してから並べ替える。
const finishedLabel = ( ... );
const remainingLabel = ( ... );
const bar = ( ... );
if (layout === "row") {
return (
<div className="flex items-center gap-2.5">
<div className="min-w-0 flex-1">{bar}</div>
{finished ? finishedLabel : remainingLabel}
</div>
);
}
ここで避けたかったのは、レイアウトごとに同じJSXを書き写すことです。書き写すと、表示内容を変えたときに片方だけ直し忘れます。部品を変数にしておけば、並べ替えても中身は1つのままです。
飛んだ先が、バーの裏に隠れる
実装して気づいた副作用です。
目次のリンクを押すと、その見出しまでスクロールします。ところが貼り付いたバーの裏に見出しが潜り込んで見えなくなりました。ブラウザは「見出しを画面の一番上に持ってくる」ことしかしないので、上に何かが被さっていることを知りません。
@media (max-width: 1023px) {
.wp-content h2,
.wp-content h3 {
scroll-margin-top: 8.5rem; /* ヘッダー + 目次バー + 余白 */
}
}
scroll-margin-top は「スクロールで停止する位置を、この分だけ手前にずらす」指定です。PCには目次バーが無いので、幅の狭い画面だけに適用しています。1024pxという境目は、目次がサイドバーからバーに切り替わる位置に合わせました。
この不具合が厄介なのは、スクロール自体は正しく動いていることです。見出しは画面外ではなく、バーの1段下に隠れているだけ。しかし使う側からは「押したのに反応しない」としか見えません。動作の正しさと、体験の正しさは別だという例です。
開いたときの高さも止める
もうひとつ。目次を開いたときの高さにも上限が要ります。
見出しが8個ある記事だと、開いた目次は約340pxありました。ヘッダーと合わせて約400px。667pxの画面では、飛び先を選んでいる間、本文がほとんど見えません。
max-h-[50vh] overflow-y-auto
画面の半分までに制限し、それを超える場合は目次の中だけをスクロールさせます。これで、どんなに見出しが多い記事でも本文が半分は見えたままになります。
記事が終われば、消える
最後にひとつ、意図してそうしている挙動があります。
この目次バーは記事本文のカードの中に置いてあります。position: sticky は親要素の範囲でしか効きません。だから記事本文が終わると、バーも一緒に流れて消えます。
画面の最上端に固定する方法もありましたが、そうすると読み終えた後の「前後の記事」や「一覧へ戻る」を見ているときにも居座り続けます。もう使わないものが場所を取り続けるのは、狭い画面では特に邪魔です。
今回の学び
まず、PCでうまくいった置き方が、スマホでも良いとは限らないこと。PCの追従目次は本文の面積を削りませんが、スマホでは削ります。同じ機能でも、払う代償が違います。
次に、消す前に並べ替えを試すこと。場所が足りないときに情報を削ると、機能が減ります。縦を横にするだけで、失わずに半分の高さに収まりました。
そして、動作の正しさと体験の正しさは別だということ。目次リンクのスクロールは仕様どおり動いていましたが、使う側には「反応しない」と見えていました。正しく動いているかだけを確認していると、この種の問題は見つかりません。
最後に、貼り付くものには終わりを決めておくこと。ずっと画面にあるものは、必要な間だけあればいい。記事の中でだけ生きて、読み終えたら消える——親要素の範囲で自然にそうなる形を選びました。