模写初級 #005|ページトップボタンの作り方


ページトップボタンとは、ページを下までスクロールしたときだけ現れ、押すと最上部へ戻るリンクのことです。この課題では、そのボタンを「戻り先の指定」「画面への固定」「出し入れの切り替え」という3つの部品に分解して作ります。

模写初級 #005 は、アンカーナビ(ページ内リンク)付きの1ページ構成を題材にした課題です。見た目を1pxまで合わせることが目的ではありません。「押したら意図した場所に、意図した速さで、誰でも移動できる」という状態を自力で作れるようにするのがゴールです。

項目内容
難易度初級
所要時間目安2〜3時間(実装量から見積もった目安であり、計測値ではありません)
使う技術HTML/CSS(position・scroll-behavior・メディアクエリ)。JavaScriptは任意
模写対象beginner 005 サンプルサイト(2026-08-02時点で表示を確認)

課題の完成条件(合格ライン)

「なんとなく動いた」で終わらせないために、先に合格ラインを決めておきます。次の5つがすべて自分で確認できたら、この課題は完成です。

合格を判定する5つの基準

  1. ページ最上部ではボタンが見えず、少しスクロールすると現れ、また上に戻ると消える
  2. ボタンを押すと最上部に戻る。ブラウザのコンソールで window.scrollY0 になっている
  3. キーボードだけで操作できる。Tabキーでボタンにフォーカスが当たり、Enterで最上部へ戻る。しかもボタンが非表示のときはTabで到達しない
  4. OSの「動きを減らす」設定をONにすると、スクロールがアニメーションせず一瞬で終わる
  5. ヘッダーのナビを押したとき、飛び先の見出しが固定ヘッダーの下に潜っていない

3番目はマウスだけで作っていると絶対に気づけません。DevToolsのコンソールで次を実行すると、目視に頼らず判定できます。ボタンが非表示のときに false が返れば正しい状態です。

const btn = document.querySelector('.back-to-top');
btn.focus();
console.log(document.activeElement === btn);

外れたときに疑うこと

  • 基準3が通らない(非表示なのにフォーカスが当たる)→ ボタンの隠し方が opacity: 0 だけになっていないかを見る
  • 基準4が変わらない → prefers-reduced-motion のメディアクエリを書いたかを見る
  • スクロールが滑らかにならない → scroll-behaviorbody に書いていないかを見る
  • 基準5が潜る → 飛び先のセクションに scroll-margin-top が指定されているかを見る

疑う先は1つだけにしてください。「そこを直して、もう一度同じ基準で測る」を繰り返すほうが、原因を何個も並べて考えるより早く終わります。


差分チェックツールで効率UPお手本コードと自分のコードを比較して、違いを一目で確認できます。練習前にブックマークしておくと便利です。
Diff Checkerを開く →

模写サンプルの構造を読む

サンプルは、固定ヘッダー・ヒーロー・About・Services・Works・Contact・フッターという構成の1ページです。ナビの4リンクはすべてページ内リンクで、右下に「↑」のボタンが置かれています。

模写に入る前に2点だけ押さえておくと迷いません。ヘッダーのナビは画面幅768px未満では消え(ハンバーガーメニューは無く、SPではフッターのリンクが入口になります)、Servicesの3カラムは768px以上でだけ3列になります。つまりモバイルファースト、SPから組んでPCを足す順番が合っています。

HTMLの骨格はこの程度で十分です。装飾は後回しにして、まずは戻り先とリンクの関係だけを正しく作ります。

<header class="site-header">
  <nav>
    <a href="#about">About</a>
    <a href="#services">Services</a>
  </nav>
</header>

<main>
  <section id="about">...</section>
  <section id="services">...</section>
</main>

<a href="#top" class="back-to-top" aria-label="ページの先頭へ戻る">↑</a>

ページトップボタンを3つの部品に分解する

ページトップボタンは1つの機能に見えますが、中身は独立した3つの部品です。分けて作ると、どこで壊れたのかが分かります。

部品1・戻り先を決める

戻り先は id で指定します。ただしページ最上部へ戻すだけなら、専用の id を用意する必要はありません。MDNは「href="#top" または空のフラグメント(href="#")を使用すると、現在のページの先頭にリンクすることができると、HTML仕様書で定義されています」と明記しています(MDN: <a>要素・2026-08-02取得)。

ボタンにする要素は <button> ではなく <a> にしておくと、JavaScriptが読み込まれる前でも押せば戻ります。中身が記号1文字(↑)になるので、aria-label で「ページの先頭へ戻る」と補う点もサンプルどおり真似してください。

部品2・画面に固定する(fixedとstickyの違い)

右下に常に浮かせるなら position: fixed です。sticky と迷いやすいので、MDNの定義で違いを押さえておきます(MDN: position・2026-08-02取得)。

ふるまいこの課題での用途
fixed通常フローから外れ、レイアウト上の場所を確保しない。ビューポートを基準に配置されるページトップボタン、固定ヘッダー
sticky通常フローに残ったまま、スクロールに応じて基準位置からずれる。最も近いスクロール可能な祖先に貼りつくこの課題では使わない

sticky は「元の位置がある」のが前提なので、常時右下に浮かせる用途には向きません。なお stickytop などのインセットを1つ以上 auto 以外にしないと貼りつかない点も、うまく動かないときの定番の原因です。

部品3・出し入れを切り替える

ここが、この課題でいちばん差がつくところです。opacity: 0 だけで隠すと、要素は生きたまま透明になるだけなので、キーボードのTabキーで見えないボタンにフォーカスが移ってしまいます。サンプルのCSSを一時的に書き換えて測ったところ、opacity: 0 のみでは focus() が通り、visibility: hidden を併用した本来の状態ではフォーカスが移りませんでした(document.activeElementbody のまま)。

仕様どおりの挙動です。MDNは visibility: hidden の要素について「(タブ順で操作された時などに)フォーカスを受け取ることができません」と説明しています(MDN: visibility・2026-08-02取得)。フェードさせたいなら、opacityvisibility を両方切り替えるのが正解です。

.back-to-top {
  position: fixed;
  right: 24px;
  bottom: 24px;
  opacity: 0;
  visibility: hidden;
  transition: opacity .3s ease, visibility .3s ease;
}

.back-to-top.is-visible {
  opacity: 1;
  visibility: visible;
}

is-visible を付け外しする部分だけがJavaScriptの仕事です。スクロール量がしきい値(サンプルは300px)を超えたらクラスを足し、下回ったら外します。ここまで分解できていれば、JavaScriptは10行程度で済みます。


スムーズスクロールはCSSだけで足りる

滑らかなスクロールにJavaScriptは要りません。scroll-behavior: smooth の1行で足ります。MDNのBaseline表示では「広く利用可能」で、2022年3月から主要ブラウザで使えるとされています(MDN: scroll-behavior・2026-08-02取得)。

書く場所を間違えると効かない

MDNは「このプロパティがルート要素に指定された場合は、代わりにビューポートに適用されます。このプロパティが body 要素に指定された場合は、ビューポートには適用されません」と書いています。つまり body { scroll-behavior: smooth } は効きませんhtml に書く必要があります。

サンプルページで実際に測ってみました。html に指定した状態でナビを押すと、30ミリ秒後のスクロール位置は 1px(=まだ動いている途中)で、1.5秒後に目標の 1775px へ到達します。ところが htmlauto に戻して bodysmooth を書くと、30ミリ秒後にはすでに 1775px。アニメーションせず一瞬で飛んでいることが数値で分かります。

速度は指定できない

smooth の動きは「ユーザーエージェントが定義したイージング関数と時間」で行われるとMDNに書かれています。CSSからスクロール速度を調整することはできません。秒数を細かく詰めたいなら、そこで初めてJavaScriptの出番になります。同じくMDNには「ユーザーエージェントは、このプロパティを無視することができます」とも書かれているので、スムーズスクロール前提の設計にはしないでください。


動きを減らしたい人への配慮

大きな画面が勢いよく流れる動きは、前庭障害のある人にとって不快感の引き金になり得ます。MDNの prefers-reduced-motion の解説にも、拡大縮小や大きな対象の移動が引き金になり得ると明記されています(MDN: prefers-reduced-motion・2026-08-02取得)。この機能はBaseline「広く利用可能」で、2020年1月から主要ブラウザで使えます。

重要なのは、ブラウザが自動でスムーズスクロールを止めてくれるわけではないという点です。Chromium 150 で「動きを減らす」をエミュレートして測ったところ、設定のON・OFFにかかわらずスクロールは同じようにアニメーションしました(どちらもクリック40ミリ秒後は 1px、1.6秒後に目標の 1604px)。書き手が明示的に止める必要があります。

html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

この3行を足して同じ測定をすると、設定がONのときはクリック40ミリ秒後にはもう 1604px に到達していました。アニメーションが完全に消えているということです。ボタンのフェード用 transition も同じメディアクエリの中で none にしておくと一貫します。

自分の環境で確かめるには、macOSなら「システム設定 → アクセシビリティ → 視差効果を減らす」、Windows 11なら「設定 → アクセシビリティ → 視覚効果 → アニメーション効果」を切り替えます。模写サンプルのCSSにはこのメディアクエリが1つも書かれていないので、ここは自分で足す加点ポイントです。


つまずきポイント

実際にサンプルページ上で再現できたものだけを挙げます。数値は Chromium 150・画面幅1280pxでの実測値です。

症状原因直し方
ナビを押しても滑らかにならず一瞬で飛ぶscroll-behaviorbody に書いている(ビューポートに伝わらない)html に移す
飛び先の見出しが固定ヘッダーの下に隠れる飛び先に scroll-margin-top が無い。ヘッダー高71.6px分だけ潜る飛び先にヘッダー高以上の scroll-margin-top を指定する
非表示のはずのボタンにTabでフォーカスが入るopacity: 0 だけで隠している(要素は生きている)visibility: hidden を併用する
「動きを減らす」設定でも滑らかスクロールが止まらないブラウザは自動では止めないprefers-reduced-motionauto に戻す

2番目は数値で確かめられます。サンプルの固定ヘッダーの高さは 71.59px。Servicesセクションは上に 120px の余白を持つため、飛んだあと見出しはヘッダーの 48.4px 下に着地し、たまたま隠れずに済んでいます。余白を 0 にすると見出しは 71.6px 分ヘッダーに潜り、そこへ scroll-margin-top: 90px を足すと 89.99px の位置、つまりヘッダーの 18.4px 下に戻りました。「余白で偶然かわしている」のか「意図して制御している」のかを区別できるようになるのが、この課題の裏テーマです。

scroll-margin-top もBaseline「広く利用可能」で、2021年4月から主要ブラウザで使えます(MDN: scroll-margin-top・2026-08-02取得)。CSSでの余白調整は CSSテクニックのまとめもあわせて読むと引き出しが増えます。


進め方の手順

上から順に進めると、「何が壊れたか分からない」状態になりません。各ステップの終わりに、合格ラインのどれが満たせたかを確認してください。

  1. CSSを書かずにHTMLだけ作る。ナビのリンクを押して各セクションへ飛べることを確認する
  2. ページ下部に href="#top" のリンクを置く。まだ固定しない。押して最上部へ戻ることを確認する
  3. position: fixed で右下に固定する。ここで合格ライン2をクリアできる
  4. opacityvisibility で初期状態を隠し、JavaScriptでクラスを付け外しする。合格ライン1と3を確認する
  5. htmlscroll-behavior: smoothprefers-reduced-motion を足す。合格ライン4を確認する
  6. 飛び先に scroll-margin-top を足し、SPとPCの両方で見出しが潜らないことを確認する(合格ライン5)

レイアウトの余白や文字サイズの整え方でつまずいたら、Webデザインの基本ルールに戻って、そろえる・まとめる・余白を取るという原則から見直すと早く直せます。


次の課題へ

この #005 は初級の最後の課題です。ここまでで、ページ内リンク・固定表示・状態の切り替え・動きへの配慮という、実務でそのまま使う4点が一通りそろいました。

ひとつ前の課題は 模写初級 #004 ブログ一覧レイアウトです。カード型の並びに不安が残っていれば、先にこちらを通してから戻ってきてください。

初級を終えたら次は準中級です。模写コーディング課題の一覧から準中級の課題を選んでください。レベル間には難易度の段差があるので、番号順に進むより一覧で全体を見てから選ぶほうが詰まりません。


よくある質問(FAQ)

Q. ページトップボタンはJavaScriptなしでも作れますか?

作れます。<a href="#top"> で最上部へ戻り、position: fixed で右下に固定し、htmlscroll-behavior: smooth を書けば、HTMLとCSSだけで動くページトップボタンになります。JavaScriptが必要になるのは「一定量スクロールしたときだけ表示する」という出し入れの制御部分だけです。

Q. 戻り先の id は必ず用意しないといけませんか?

ページ最上部へ戻すだけなら不要です。MDNは href="#top" または空のフラグメント href="#" で現在のページの先頭へリンクできるとHTML仕様書で定義されている、と説明しています。ただし id="top" を持つ要素がページ内にある場合はそちらが優先されるため、意図しない位置に飛ぶときはその重複を確認してください。

Q. ボタンを opacity だけで隠してはいけないのはなぜですか?

透明になるだけで要素は存在し続けるため、キーボードのTabキーで「見えないボタン」にフォーカスが移ってしまうからです。MDNは visibility: hidden の要素はタブ順で操作されたときにフォーカスを受け取れないと明記しており、実際に測っても opacity: 0 のみではフォーカスが通り、visibility: hidden を併用した状態では通りませんでした。

Q. スムーズスクロールの速度を変えることはできますか?

CSSではできません。MDNによると smooth の動きはユーザーエージェントが定義したイージング関数と時間で行われるため、作り手が秒数を指定する手段はありません。速度を制御したい場合は scroll-behavior を使わず、JavaScript側でスクロール量を自前で補間する実装に切り替えることになります。

Q. 動きを減らす設定への対応は、ブラウザが自動でやってくれませんか?

自動ではやってくれません。Chromium 150 で「動きを減らす」設定をエミュレートして測定したところ、設定のON・OFFでスクロールの挙動は変わらず、どちらもアニメーションしました。@media (prefers-reduced-motion: reduce) の中で scroll-behavior: auto に戻すコードを、書き手が自分で足す必要があります。

Q. 見た目をサンプルと完全に一致させる必要はありますか?

ありません。この課題の目的は、ページ内リンクとページトップボタンの仕組みを自分の手で組み立てることです。色・フォント・画像が違っていても、合格ラインの5項目を自分で判定できていれば完成とみなして問題ありません。1pxを合わせる練習は、上位レベルの課題で扱います。