模写準中級 #004 | ページ最下部に貼り付くフッター


フッターの模写でつまずくのは、カラムを横に並べる部分ではありません。並べ終わったあとに「本文が短いページだと、フッターが画面のまん中で止まる」ところです。

模写準中級 #004 は、複数カラムのフッターを組んだうえで、本文が1行しかないページでもフッターがビューポートの最下部に到達する状態まで作りきる課題です。横並びの再現だけでは合格になりません。完成コードの全文は載せず、骨格と、書いたものが条件を満たしているか自分で判定する手順だけを置いています。


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

この課題で作るもの

項目内容
難易度準中級
所要時間目安2〜3時間
使う技術HTML / CSS(Flexbox または Grid)
作るもの4カラムのフッター+コピーライト行
合格の核心本文が1行でもフッターが最下部に来る

所要時間は実装量から見積もった目安で、計測値ではありません。模写元は会社概要・サービス・サポート・お問い合わせの4カラムで、幅が足りなくなると折り返し、狭い画面では1列になります。

模写サンプル  Flexboxフッター(elementary 004)

色や文言は自分で決めて構いません。ただしサンプルはフッター単体を置いたページなので、模写するときは必ずヘッダーと本文を持つ1枚のページに組み込んでください。核心はフッター単体ではなく、ページ全体の縦方向の組み立てにあります。


完成条件(合格ライン)

判定は目視ではなく数字で行います。自分のページをブラウザで開き、開発者ツールのコンソールに次の4つを順に貼ってください。

判定1|本文が1行でもフッターが最下部に来る

// 本文の段落を1つだけ残した状態で実行する
const f = document.querySelector('footer').getBoundingClientRect();
console.log(Math.round(f.bottom), window.innerHeight);

合格ライン: 2つの数字が一致すること(1px程度の差は許容)。左がフッターの下端、右がビューポートの高さです。左が小さければ画面の途中で止まっています。外れたら、ページ全体を包むラッパーに高さの土台(min-height)があるかを疑ってください。

判定2|本文が長いとき最後の行が隠れない

// 本文を画面に収まらない量まで増やし、最下部までスクロールしてから実行する
const ps = [...document.querySelectorAll('main p')];
const last = ps[ps.length - 1].getBoundingClientRect();
const f = document.querySelector('footer').getBoundingClientRect();
console.log(last.bottom <= f.top);

合格ライン: true が出ること。false なら本文の最後の行がフッターの下敷きになっています。外れたら、フッターに position: fixed を使っていないかを疑ってください。

判定3|狭い画面で横スクロールが出ない

// 開発者ツールの端末エミュレーションで幅を320pxにして実行する
console.log(document.documentElement.scrollWidth, window.innerWidth);

合格ライン: 2つの数字が一致すること。左が大きければ、どこかが画面幅からはみ出しています。外れたら、カラムに box-sizing: border-box が効いているかを疑ってください。

判定4|折り返した最後のカラムが横長にならない

// カラムが2行に折り返す幅にして実行する(クラス名は自分のものに置き換える)
console.log([...document.querySelectorAll('.footer-col')]
  .map(e => Math.round(e.getBoundingClientRect().width)));

合格ライン: 並んだ数値がすべて同じくらいであること。最後の1つだけ極端に大きければ、折り返した行の余白をその要素が独り占めしています。外れたら、flex の1つ目の値(flex-grow)を疑ってください。


実装の骨格

ページ全体をラッパーで包み、その中を3段に分けるのが基本形です。

<body>
  <div class="page">
    <header>...</header>
    <main>...</main>
    <footer class="site-footer">
      <div class="footer-inner">
        <div class="footer-col">...</div>
        <div class="footer-col">...</div>
        <div class="footer-col">...</div>
        <div class="footer-col">...</div>
      </div>
      <p class="copyright">...</p>
    </footer>
  </div>
</body>

カラムを包む .footer-inner をもう1枚挟んでいるのが要点です。コピーライトをカラムと同じ階層に置くと、それが5つ目のカラムとして扱われ、折り返しが崩れます。

最下部に置く方法は2つある

MDNのレイアウトレシピ集は、この形を「コンテンツが短いときはビューポート下端に貼り付き、長いときは通常どおり本文の下へ押し下げられること」と定義し、Grid版とFlexbox版の2つを挙げています。

html, body { margin: 0; }

/* Grid版: 中段だけを伸ばす */
.page {
  min-height: 100dvh;
  display: grid;
  grid-template-rows: auto 1fr auto;
}
html, body { margin: 0; }

/* Flexbox版: フッターの上の余白を自動で埋める */
.page {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
.site-footer { margin-top: auto; }

Grid版は3段の役割がそのままCSSに現れ、あとから段を増やしても壊れにくい書き方です。Flexbox版が使っているのは、主軸方向の余白をauto marginで吸わせる性質で、MDNは「主軸には justify-self が無いので、代わりにauto marginで個別に位置を寄せられる」と説明しています。どちらか片方でよいので、両方は書かないでください。出典は MDN「Sticky footers」MDN「Aligning items in a flex container」(いずれも2026-08-02取得)です。

カラムの折り返しはflexの1つ目の値で決まる

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}
.footer-col {
  flex: 0 1 22%;   /* grow / shrink / basis */
  box-sizing: border-box;
}

1つ目の値が flex-grow です。ここを1にすると、折り返して1つだけになった行でその要素が余白を全部吸い、横長になります。基準幅を保ちたいなら0にします。狭い画面で1列にしたいときは、メディアクエリで3つ目の値(flex-basis)を100%に変えるだけで済みます。


100vhではなく100dvhを使う理由

スマートフォンのブラウザは、スクロールに応じてアドレスバーを引っ込めたり出したりします。表示領域の高さが1つに定まらないため、CSSはビューポート高さの単位を3種類に分けています。

単位基準にする高さ性質
svhブラウザUIが出ているときの小さい方固定・安定。はみ出しにくい
lvhブラウザUIが隠れたときの大きい方固定・安定。UI出現時に隠れうる
dvhそのときの実際の高さ可変。ぴったり収まる

問題は vh がどれに当たるかです。MDNは「現時点で、既定のビューポート単位(vh、vwなど)はすべて、大きい方の対応単位(lvh、lvwなど)と等価である」と明記しています。つまり 100vh はアドレスバーが引っ込んだ状態の高さで、アドレスバーが出ている間はその分だけ画面からはみ出します。1行しか本文が無いのに縦スクロールが出る、という形で表面化します。

そこで実際の高さに追従する dvh、または小さい方で安全側に倒す svh を使います。ただしMDNは、dvhにはスクロール中に寸法が変わってUIが揺れる副作用があるとも書いています。はみ出さないことを最優先したい箇所では svh のほうが素直です。

対応状況は、Safari 15.4(2022-03-14)、Firefox 101(2022-05-31)、Chrome 108(2022-11-29)で、svh・lvh・dvhが同時に入っています。Web Platform Status では Baseline の「widely available」です。出典は MDN「<length>」Web Platform Status「viewport-unit-variants」(いずれも2026-08-02取得)です。ただし実機でアドレスバーが伸縮したときの見え方はこの記事では検証していません。手元のスマートフォンで単位を入れ替えてスクロールすれば、違いはすぐ分かります。


フッターをposition: stickyにしてはいけない理由

「最下部に貼り付く」と聞くと position: sticky を思いつきますが、別物です。MDNの定義では、stickyな要素は「包含ブロックが指定のしきい値を越えるまでは相対配置として扱われ、越えた時点で固定された状態になる」ものです。スクロールが発生しない短いページには、越える機会そのものがありません。

ビューポート390×700px、フッターの高さ64pxで測った結果が次です。

指定本文1行のときの下端本文が長いとき
何も指定しない144px(画面下端は700px)本文の下に正しく続く
position: sticky; bottom: 0144px(変わらない)本文の2段落が常時隠れる
position: fixed; bottom: 0700px最下部の2段落が隠れる
min-height: 100dvh + Grid700px本文の下に正しく続く
min-height: 100dvh + Flex700px本文の下に正しく続く

stickyは本文が短いときの位置をまったく変えず、しかも本文が長いときは読んでいる間ずっと画面下を占領します。fixedは短いページでは下端に来ますが、こんどは最下部の本文が下敷きになります。どちらも2つの要件を同時には満たしません。計測は Playwright の Chromium 151.0.7922.34、2026-08-02。stickyの定義の出典は MDN「position」(同日取得)です。


footer・address・navの使い分け

フッターは住所・リンク一覧・コピーライトが同居する場所です。全部 div で組んでも見た目は同じですが、仕様上の線引きは決まっています。

要素入れるもの入れてはいけないもの
footer著者情報・コピーライト・関連文書へのリンクfooterやheaderの入れ子
address連絡先(住所・電話・メール等)と相手の名前連絡先以外の情報(公開日など)
nav主要なナビゲーションリンクのまとまりページ上のあらゆるリンク

実装で迷いやすい2点

1つ目はコピーライトの扱いです。MDNは address について「連絡先以外の情報(公開日など)を含めるべきではない」と書いています。コピーライト表記は連絡先ではないので address の外に置き、会社概要カラムの住所・電話・メールだけを address にまとめます。入れ子の向きも決まっていて、footer の中に address を置くのは正しい形ですが、address の中に footer を置くことは許されていません。

2つ目は nav です。ここは誤解されやすく、MDNは「すべてのリンクを nav に入れる必要はない。nav は主要なナビゲーションリンクのまとまりのためのもので、footer 要素が持つリンクの一覧は通常 nav に入れる必要がない」と明記しています。サービス一覧・サポート一覧といったフッター内のリンク群は ul のままで構いません。サイトの主要ナビとして扱う場合だけ nav を使い、そのときはヘッダー側と区別できるようラベルを付けます(MDNは複数の nav がある場合に aria-labelledby を挙げています)。

なお footer の意味は置き場所で変わります。MDNの定義は「もっとも近い祖先のセクショニングコンテンツ、またはセクショニングルートに対するフッター」で、記事の中に置けばその記事の、body 直下ならページ全体のフッターになります。出典は MDN「<footer>」MDN「<address>」MDN「<nav>」(いずれも2026-08-02取得)です。


つまずきポイント

この課題の構成を実際に組んで再現できた3つです。数値は390×700pxのビューポートでの実測値です。

症状原因直し方
本文が1行だとフッターが画面の途中で止まる(下端144px/画面700px)ページ全体に高さの土台が無く、中身の高さぶんしか伸びていないラッパーに min-height を与え、Gridなら中段を1fr、Flexならフッターに margin-top: auto(下端700pxになる)
内容が1行なのに縦スクロールバーが出る(文書716px/画面700px)bodyの初期marginが上下8pxずつ残り、100dvhに16px上乗せされているbody の margin を0にする(文書700pxになり、スクロールバーが消える)
折り返した最後のカラムだけが極端に横長になる(860px/他は203px)flex-grow が1で、折り返した行の余白をその要素が独占しているflex-grow を0にする(189pxで揃う)。Gridの repeat(auto-fit, minmax()) でも揃う(203px)

2つ目は特に見落としやすい症状です。min-height: 100dvh はビューポートと同じ高さを作りますが、その外側に body のmarginが残っていれば、合計は必ずそれより大きくなります。


次に進む課題と関連記事

余力があれば足す発展課題

  • 会社概要カラムの住所・電話・メールを address で囲み直す
  • min-height を 100svh に変え、判定1の数値が変わらないことを確認する
  • Flexbox版で組んだものをGrid版に書き換え、判定1から判定4をもう一度通す

前後の課題

1つ前は 模写準中級 #003 | Gridセクション です。ここで使った grid-template-rows の考え方は、そちらで扱う2次元のグリッドと地続きです。次は 模写準中級 #005 | スクロールアニメーション。フッターまで含めて1枚のページを組めるようになったら、そこに動きを足す段階に進みます。他のレベルの課題は 模写コーディング一覧 にまとめてあります。

手が止まったときに読むもの

レイアウトの指定そのものがあやふやなら、CSS実装テクニック集 に基礎から設計手法までをまとめてあります。flex の3つの値の効き方は、値をその場で切り替えて確かめるのがいちばん早いです。

Flexboxジェネレーター(別タブで開きます)


よくある質問(FAQ)

Q. フッターが画面のまん中で止まります。何が足りませんか?

ページ全体を包む要素に高さの土台がありません。ラッパーに min-height: 100dvh を与え、Gridなら grid-template-rows: auto 1fr auto で中段を伸ばし、Flexboxなら flex-direction: column にしてフッターに margin-top: auto を指定すると、本文が1行でもフッターの下端がビューポート下端に一致します。

Q. position: sticky で最下部に貼り付けてはいけませんか?

この課題の要件は満たせません。sticky はしきい値を越えるまで相対配置として扱われる仕様のため、スクロールが発生しない短いページではフッターの位置が1pxも動かず、長いページでは読んでいる間ずっと画面下を占領して本文を隠します。実測でも、本文1行のときのフッター下端は指定なしと同じ144pxのままでした。

Q. フッターのリンク一覧は nav で囲むべきですか?

囲まなくて構いません。MDNは、すべてのリンクを nav に入れる必要はなく、footer が持つリンクの一覧は通常 nav に入れる必要がないと明記しています。サイトの主要ナビゲーションとして扱いたい場合だけ nav を使い、そのときはヘッダー側と区別できるようラベルを付けます。

Q. コピーライトは address で囲むのが正しいですか?

正しくありません。MDNは address について、連絡先以外の情報を含めるべきではないと書いています。住所・電話・メールなどの連絡先だけを address にまとめ、コピーライト表記は p や small で書きます。

Q. 内容が1行しかないのに縦スクロールバーが出ます。

body の初期marginが残っています。上下8pxずつが min-height: 100dvh に上乗せされ、文書の高さがビューポートより16px大きくなるためです。実測では716pxと700pxでした。body の margin を0にすると一致します。

Q. 折り返したときに最後のカラムだけ横長になります。

flex の1つ目の値(flex-grow)が1になっています。折り返して1つだけになった行では、その要素が余白をすべて吸い込みます。実測では他が203pxのところ最後だけ860pxになりました。flex-grow を0にすると189pxで揃います。