0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

表示を切り替えるとページの高さが変わる — 読んでいた場所へ戻す

0
Last updated at Posted at 2026-08-24

こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。

このサイトの記事には、表示を切り替えるボタンが付いています。文字の大きさ、書体、縦書きか横書きか、ふりがなの有無です。

読んでいる途中で押されることを想定しています。ところが、押すと読んでいた場所を見失いました。

なぜずれるか

これらの設定は <html> の属性を変えることで効きます。CSS がページ全体に当たるので、そのページにある全部の記事が再レイアウトされます。

文字を大きくすれば、各記事が縦に伸びます。押したボタンより上にある記事が伸びると、そのぶん下へ押し出されます。読んでいた場所が画面外へ出ます。

ふりがなの表示でも同じことが起きます。ルビが出ると行の高さが変わります。

厄介なのは、「その記事自体が画面に収まっている」ときでもずれることです。ページ全体の高さが変わるので、スクロール位置の意味が変わります。

押した記事の先頭へ合わせ直す

対処は、切り替えた後に、押したボタンがある記事の先頭へスクロールし直すことでした。

// トグルは <html data-kn-*> 経由でページ内の全記事を再レイアウトするため、
// バー(記事ヘッダ帯)より上の高さが変わりスクロール位置がズレる。
// 記事全体が切替前の画面内に収まっていてもページ全体の高さは変わるため、
// 再描画後は常に押した記事の Paper(scroll-margin-top 付き)の先頭へ合わせ直す。
// rAF は React のコミットと再レイアウト完了後にスクロールを確定するため。
const updateDisplayAndScrollToArticle = (update: () => void) => {
  const article = navRef.current?.closest('[id^="art-"]');
  if (!article) {
    update();
    return;
  }
  update();
  requestAnimationFrame(() => {
    article.scrollIntoView({ block: 'start' });
  });
};

やっていることは 3 つです。

  1. 押されたボタンから、自分が属する記事の要素を探す
  2. 設定を更新する
  3. 次のフレームで、その記事の先頭へスクロールする

rAF が要る理由

設定を更新した直後にスクロールしても、意味がありません。その時点ではまだ再レイアウトが終わっていないので、古い座標へ移動します。

requestAnimationFrame を挟むと、React の更新が DOM に反映され、ブラウザがレイアウトを計算した後に実行されます。

固定の待ち時間(setTimeout(0) や 100ms)でも似たことはできますが、環境によって足りたり足りなかったりします。フレームの区切りに合わせるほうが確実でした。

記事が見つからないとき

ボタンは、記事の中だけでなくページの先頭にも置いています。トップページや月別のページでは、記事に属していません。

その場合は、スクロールの調整をせずに設定だけ更新します。

if (!article) {
  update();
  return;
}

同じ関数を両方の場面で使うので、対象が無いケースを最初に返しています。無理に何かへスクロールさせるより、何もしないほうが自然でした。

scroll-margin-top と組み合わせる

スクロール先の記事には、固定ヘッダに隠れないための余白が指定してあります。

scrollMarginTop: ANCHOR_SCROLL_MARGIN_TOP,

scrollIntoView はこの指定を尊重するので、ヘッダの下に潜りません。目次から飛んだときと同じ着地になります。

同じ「特定の要素へ移動する」でも、経路が 2 つ(目次からのリンクと、この処理)あります。着地の位置を決めるのは要素側の指定なので、どちらから来ても同じ位置に着きます。

移動する側ではなく、移動先の要素に着地位置を持たせると、経路が増えても揃います。

縦書きでは別の処理

縦書きの本文は、横方向にスクロールする独立したブロックです。設定を変えると、この中のスクロール位置もずれます。

こちらは別に、本文の先頭へ戻す処理を入れています。

const scrollToStart = () => { … };
scrollToStart();
const frame = requestAnimationFrame(scrollToStart);

2 回呼んでいるのは、即座に効かせつつ、レイアウト確定後にもう一度確定させるためです。1 回目で見た目が動き、2 回目で正確な位置になります。

読んでいた位置を保つ、という選択肢もありました。ただ、縦書きの列は組み直しで完全に変わるので、元の位置に対応する場所がありません。先頭に戻すほうが分かりやすいと判断しました。

どこまで戻すのが正しいか

「読んでいた場所」を厳密に復元するなら、表示中の段落や文字を覚えておいて、そこへ戻すことになります。

やりませんでした。記事 1 本がそれほど長くないので、先頭に戻せば十分に近い場所です。実装も単純で済みます。

厳密さと単純さの折り合いは、コンテンツの長さで決まります。長い記事を扱うサイトなら、別の判断になったと思います。

まとめ

  • ページ全体に効く設定を変えると、そのページの全要素が再レイアウトされてスクロール位置がずれる
  • 対象の記事が画面に収まっていても、ページ全体の高さが変わるのでずれる
  • 操作したボタンが属する要素の先頭へ、切り替え後に合わせ直す
  • スクロールは requestAnimationFrame の中で行う。更新直後では古い座標になる
  • 着地位置は移動先の要素に持たせる(scroll-margin-top)。経路が増えても揃う
  • 厳密な位置の復元より、先頭に戻すほうが単純で十分なことがある
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?