模写中級 #001「ポートフォリオ」は、4枚のカードをCSS Gridで自由な位置に敷き詰め、マウスを乗せると隠れていた説明文が開き、スクロールすると順に浮かび上がる、ダークテーマの1ページ・ポートフォリオを作る課題です。使うのはHTML・CSS・素のJavaScriptだけで、ライブラリは使いません。
中級の1本目です。初級との違いは「タグを正しく置けるか」ではありません。グリッドのライン番号を自分で決められるかと、hoverやクラス付与といった「状態」でCSSを切り替えられるか——この2点に絞られます。動く見本は 模写中級 #001 の完成見本 で確認できます。
課題の条件は次のとおりです。所要時間は実装量から見積もった目安であり、計測した数値ではありません。
| 項目 | 内容 |
|---|---|
| 難易度 | 中級 |
| 所要時間 | 目安3〜5時間(実装量からの見積もり) |
| 使う技術 | HTML/CSS Grid/transform・transition/@keyframes/clamp()/JavaScript(classList・getBoundingClientRect) |
| 作るページ数 | 1ページ(ヘッダー・カード4枚・テキストアニメーション・フッター) |
| 対応幅 | PCと、768px以下のスマートフォン表示の2段階 |
この課題で作るもの
ページは上から4つのパートでできています。それぞれで練習する技術が違うので、どこで詰まっているのかを切り分けやすい構成になっています。
| パート | 中身 | 主に使う技術 |
|---|---|---|
| ヘッダー | 大きな英字タイトルと、薄く重ねたゴースト表記 | position: sticky/@keyframes/clamp() |
| カード4枚 | 画像+見出し+説明文。ホバーで説明が開く | CSS Grid(12列)/transform/transition |
| テキストアニメーション | 1文字ずつ時間差で現れる帯 | JavaScriptで文字を分割/@keyframes |
| フッター | 戻るリンクと著作権表記 | 基本のレイアウト |
HTMLの骨格
最初に決めるのは入れ物です。カードは同じ形のものが4つ並ぶので、リストとして持ちます。ここまでを先に書き切ってから、CSSに入ってください。
<header class="site-header">
<h1>タイトル</h1>
<p aria-hidden="true">薄いゴースト表記</p>
</header>
<main>
<ul class="card-list">
<li class="card">
<img src="..." alt="...">
<div class="card-text">
<h2>見出し</h2>
<p>ホバーで開く説明文</p>
</div>
</li>
... 同じ形をあと3つ
</ul>
</main>
<footer class="site-footer">...</footer>
完成コードは載せません。模写課題は「見本を見て自分で組み立てる」ことが練習になるので、ここで出すのは骨格と、詰まりやすい箇所の直し方までにしています。
グリッドの数値仕様
カードの配置はこの課題の中心です。見本は「12列 × 8行(1行80px)、gap 16px」のグリッドを敷き、その上に4枚を次のように置いています。数値を先に固定しておくと、迷わず組めます。
| カード | grid-column | grid-row |
|---|---|---|
| 1枚目 | 1 / 6 | 1 / 4 |
| 2枚目 | 6 / 13 | 1 / 3 |
| 3枚目 | 7 / 13 | 3 / 6 |
| 4枚目 | 1 / 7 | 4 / 9 |
ここで一度つまずきます。指定するのは「列」ではなく「線」の番号だからです。12列のグリッドには線が13本引かれるので、右端まで使うカードの終端は 13 になります。6 / 13 は「6本目の線から13本目の線まで=6列目から12列目まで」という意味です。埋まらないマスがあるのは不具合ではなく、余白として意図された配置です。
768px以下では、この12列を捨てて 1fr 1fr の2列に切り替え、行の高さも auto に戻します。行を80px固定のまま狭い幅に持ち込むと、カードの中身が入りきらずに溢れます。
完成条件(ここまで出せたら合格)
「なんとなく似た」で終わらせないために、自分で合否を出せる基準を置いておきます。ブラウザの検証ツール(DevTools)を開き、次の5つを順に見てください。すべてOKなら合格です。
| 確認すること | 合格の状態 | 外れたら疑うところ |
|---|---|---|
| PC幅でのカード配置 | 4枚が重ならず、上の表の列・行に収まっている | grid-column/grid-row の終端の線番号 |
| カードにマウスを乗せる | 説明文の max-height が 0px から 200px に変わり、文章が開く | hoverを書いた要素(親か子か) |
| ページをスクロールする | 各カードに show クラスが付き、opacity が 0 から 1 になる | scrollイベントの登録と、しきい値の判定式 |
| 幅を768px以下にする | 2列になり、説明文が最初から開いている | メディアクエリの中で max-height と opacity を戻しているか |
| ヘッダーのタイトル | スクロールし始めた直後、画面上端から一定の位置で止まる | sticky に top などのオフセットを書いているか |
疑うところは1段階だけにしてあります。そこを直しても変わらないときは、原因はもう1つ外側にあります。CSSを足す前に、DevToolsのElementsパネルでクラスが付いているかどうかを先に見てください。「クラスは付いているのに見た目が変わらない」のか「クラスが付いていない」のかで、直す場所がCSS側かJavaScript側かに分かれます。
実装の順番
骨格を全部書いてからCSSに入る
カードを1枚作ってCSSを書き、また1枚足して……と進めると、グリッドの行数が途中で変わって毎回崩れます。4枚ぶんのHTMLを先に書き切ってからCSSを当ててください。この課題では、中身のテキストは仮の文字で構いません。
グリッドは線を紙に描いてから書く
12本の縦線と9本の横線を紙に引き、4枚の四角を塗ってから、線の番号を読み取ってCSSに写します。頭の中だけで数えると、ほぼ必ず1つずれます。DevToolsのElementsパネルでグリッドコンテナの横に出る目印を押すと、線番号が画面に重ねて表示されるので、書いたあとの答え合わせにも使えます。
「開く」動きは高さの上限で作る
説明文をホバーで開く動きは、display: none では作れません。表示・非表示の切り替えはアニメーションしないためです。代わりに高さの上限を 0 から十分な値へ動かします。
.card-text p {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.4s ease, opacity 0.3s ease;
}
.card:hover .card-text p {
max-height: 200px;
opacity: 1;
}
肝は .card:hover .card-text p という書き方です。マウスが乗るのは親のカード、開くのは中の段落なので、hoverは親に書き、対象は子孫として指定します。見本の実測でも、ホバー前後で max-height が 0px から 200px に変わることを確認しています。上限値は中身より大きければよく、厳密な高さを合わせる必要はありません。
スクロール検知はクラスを付けるだけにする
JavaScriptでスタイルを直接書き換えると、あとから調整するときにCSSとJavaScriptの両方を追う羽目になります。JavaScript側の仕事はクラスを1つ付けるところまでにして、見た目はCSSに任せてください。
function handleScroll() {
document.querySelectorAll(".card").forEach((el) => {
const rect = el.getBoundingClientRect();
const triggerPoint = rect.top + rect.height * 0.5;
if (window.innerHeight > triggerPoint) {
el.classList.add("show");
}
});
}
handleScroll();
window.addEventListener("scroll", handleScroll, { passive: true });
読み込み直後に一度呼んでおくのがポイントです。これが無いと、最初から画面に入っているカードが、一度スクロールするまで透明のままになります。しきい値の 0.5 は「要素の高さの半分まで画面に入ったら」という意味で、この数字を上げるほど発火が遅くなります。
マークアップの考え方
ここは誤解が広まりやすい領域なので、仕様書とMDNの記述に当たって整理します(いずれも2026-08-02取得)。
section と div の使い分け
「divは意味がないから section に置き換えるほうが良い」は誤りです。HTML Living Standard は 4.3 Sections で、section要素は汎用のコンテナ要素ではなく、スタイリング目的やスクリプトの都合だけで要素が必要な場合は div を使うことが推奨されると明記しています。MDNの section要素のページも「スタイリングのラッパーとして使うだけなら div を使うこと」と書いています。
実際の挙動でも確かめられます。MDNによれば section の暗黙のARIAロールは「アクセシブルな名前があれば region、無ければ generic」です。Chromiumのアクセシビリティツリーを取得して確認したところ、<section> の中に見出しを1つ置いただけではロールは generic のままで、aria-label を付けたものだけが region になりました(同じ中身の <div> も generic)。section に置き換えただけでは支援技術から見た情報は増えません。この課題では、カードの入れ物は div とリストのままで十分です。
見出しレベルの扱い
MDNの h1〜h6のページは、使い方の注意として「見出しレベルを飛ばさない。常に h1 から始めて h2 と続ける」「文字の大きさを変える目的で見出し要素を使わない。サイズは font-size で指定する」の2点を挙げています。理由はSEOではなく、スクリーンリーダーの利用者が見出しを飛ばしながら中身を把握する読み方をするためだ、とアクセシビリティの節に書かれています。
「1ページにh1は1つだけ」も、正確には少し違います。MDNは複数のh1はHTML標準としては許されている(入れ子でない限り)が、ベストプラクティスとは見なされないという書き方をしています。あわせて、入れ子のセクショニング要素の中に複数のh1を置く形は旧版では認められていたものの現在は不適合であること、2025年5月より前は section・article・aside・nav の中の h1 を h2 相当の大きさで描画する規定があったが、その文脈依存の既定スタイルは削除されたことも明記されています。この課題の構成なら、ヘッダーのタイトルが h1、カードの見出しが h2 で、飛ばさずに収まります。
カードは article にできるか
できます。MDNの article要素のページは「文書・ページ・アプリケーション・サイトの中で自己完結した構成物で、原則として単独で配布・再利用できるもの」と定義し、その例として商品カードを挙げています。カード1枚が単独で意味の通る紹介になっているなら、<li> の中を <article> で包む形は妥当です。ただし今回は「4枚が並んでいる」ことのほうが情報として重要なので、リストのままでも間違いではありません。どちらか一方だけが正解、という問題ではないと理解しておいてください。
改行タグで段落を作らない
カードの中でスキル名を縦に並べたくなり、<br> を連ねてしまうことがあります。MDNの br要素のページは、この要素は詩や住所のように行の分かれ方そのものに意味がある場合に使うもので、段落と段落のあいだに余白を作る目的で使ってはならない、と注記しています。アクセシビリティの節にはさらに踏み込んだ記述があり、改行タグで段落を分けるのは支援技術の利用者にとって問題があるとされています。項目が並ぶならリストにしてください。
つまずきポイントと直し方
実際にブラウザで再現できたものだけを並べます。
| 症状 | 原因 | 直し方 |
|---|---|---|
| カードが重なる/1マスずれる | 指定しているのは列ではなく線の番号 | 12列なら右端は13。終端を1つ大きく書く |
| ホバーしても文が開かない | hoverを段落側や画像側に書いている | 親のカードにhoverを書き、段落は子孫として指定する |
| スクロールしても浮かび上がらない | showクラスが付いていない | Elementsパネルでクラスの有無を見る。付いていなければscroll登録側を直す |
| 読み込み直後だけ表示されない | スクロールしないと判定処理が走らない | 登録前に判定処理を1回呼ぶ |
| ヘッダーのタイトルがすぐ流れる | stickyは親要素の範囲を出られない | 貼り付かせたい範囲そのものを親にする |
| stickyが最初から効かない | 祖先にoverflowの指定がある | 横スクロール止めをbody側へ移す |
| スマホ幅でカードから中身が溢れる | 行の高さ80px固定を狭い幅に持ち込んでいる | メディアクエリで行の高さをautoに戻す |
sticky が効く範囲を実測する
下2つは説明が要ります。MDNの positionプロパティのページによれば、sticky要素は「もっとも近いスクロール可能な祖先と、包含ブロック(もっとも近いブロックレベルの祖先)を基準に」オフセットされます。さらに、overflow が hidden・scroll・auto・overlay のいずれかである祖先が「スクロールの仕組み」を持つ祖先として扱われる、という注記もあります。ここから2つの結果が出ます。
1つ目は親の高さぶんしか貼り付かないことです。見本ページをChromium(表示領域1280×800)で計測したところ、ヘッダーの高さは474pxで、タイトルはスクロール量100〜200pxの区間では画面上端から40pxの位置で止まっていましたが、400pxまで進めると上端から-88pxになり、すでに流れていました。ずっと貼り付かせたいなら、sticky要素の親を「貼り付かせたい範囲」にする——これが直し方です。
2つ目は overflow との相性です。同条件で最小のページを組んで測ると、ラッパーのdivに overflow-x: hidden を書いた場合だけ sticky が効かなくなり(1000pxスクロールした時点で要素の上端が-1000px=一緒に流れていた)、同じ指定を body に書いた場合は上端0pxのまま貼り付いていました。横スクロールを止めたいときは、途中のラッパーではなく body 側に書いてください。
次に進む
これは中級の1本目です。ここが終わったら、複数セクションを持つサイト全体の構成に進みます。次の課題は 模写中級 #002 茶屋サンプルサイト で、レスポンシブ対応とセクション設計が主題になります。
グリッドやアニメーションで手が止まったなら、先に進むより手前を埋めたほうが速いことがあります。CSS実装テクニック集 に、Grid・transition・@keyframes を個別に扱った記事をまとめてあります。作りながら必要なところだけ引く使い方ができます。
この課題が重すぎると感じたら、初級や準中級に戻って構いません。レベルの端にいるので前の課題はありませんが、模写コーディング課題の一覧 から他のレベルの課題を見て、手が動く難易度を選び直してください。順番どおりに進める必要はありません。
よくある質問
Q. 見本とピクセル単位まで同じにする必要はありますか?
ありません。この課題で見るのは、グリッドの線番号を自分で決められるか、状態でCSSを切り替えられるかの2点です。余白やフォントサイズが多少違っても、完成条件の5項目がすべて満たせていれば合格として次へ進んでください。
Q. 画像は何を使えばよいですか?
手元にあるものでも、商用利用できるフリー素材でも構いません。重要なのは画像の中身ではなく、カードいっぱいに敷き詰める指定(幅と高さを100%にして object-fit で切り抜く)が書けているかどうかです。素材選びで時間を使わないでください。
Q. スマホではホバーが効きませんが、どうすればよいですか?
768px以下のメディアクエリの中で、説明文の高さの上限と不透明度を開いた状態に戻してください。タッチ環境ではホバーという状態が存在しないため、隠したままにすると読者が本文にたどり着けません。完成条件の4番目がこの確認にあたります。
Q. カードの入れ物は div と section のどちらが正解ですか?
この課題では div で構いません。HTML Living Standard は、スタイリング目的やスクリプトの都合だけで要素が必要なときは div を使うよう推奨しています。実測でも、見出しを入れただけの section のARIAロールは div と同じ generic のままでした。名前を与えない限り、置き換えても得られる情報は増えません。
Q. ライブラリを使って作ってもよいですか?
この課題では使わないでください。スクロール検知もアニメーションもライブラリで置き換えられますが、それでは「クラスを付ける」「状態でCSSを切り替える」という中級の要点が抜けます。ライブラリを使う課題は、同じ中級の後半に別途用意されています。
Q. どのくらい時間がかかれば普通ですか?
目安は3〜5時間ですが、これは実装量から見積もった数値で、計測した平均ではありません。長くかかっても問題はなく、判断すべきは時間ではなく「完成条件のどれで止まっているか」です。同じ項目で2時間以上動かないなら、その項目の原因欄をもう1段階外側まで広げて疑ってください。
