position: sticky入門|貼り付く要素の作り方を実例デモで解説(前編)
この記事は、CSSの position: sticky の基本と使い方を解説する前編です。「sticky が効かない」「思ったところで貼り付かない」といったハマりどころは、後編で詳しく扱います。
position: sticky は、CSSだけでスクロールに追従する要素を作れるプロパティです。JavaScriptで scroll イベントを使うような実装と比べてシンプルで、動きも滑らかになります。
この記事では、position: sticky の基本から、よく使う使用例まで、順を追って解説していきます。
position: sticky とは?
position: sticky をひと言で表すなら、「スクロールに合わせて、要素を画面の特定の位置に貼り付ける」プロパティです。
たとえば、下の動画のように、見出しが、最初は自然な位置にあり、画面上端に達すると貼り付き、次のセクションが来ると押し出されて入れ替わる動きが、CSSだけで作れます。
要素は、ふだんはふつうの要素としてスクロールで流れていきますが、貼り付いているあいだは position: fixed のように、その場に留まり続けます。
基本構文と動作の仕組み
基本構文
position: sticky を使うときは、位置を指定するプロパティ(top / bottom / left / right)のいずれかを必ず一緒に指定します。これが指定されていないと、position: sticky は何も起こりません。
実際、top か bottom を使うことがほとんどです。left / right は、横スクロールするテーブルの最初の列を貼り付けるときなど、限られたケースでしか出てきません。
最小サンプルはこちらです。
.sticky-element {
position: sticky;
top: 0;
}これで、.sticky-element はスクロールに合わせて、画面の上端から 0px の位置で貼り付くようになります。
仕組み
position: sticky の動きをコントロールするために、押さえておきたいキーワードが2つあります。
- スクロールコンテナ(scroll container)
- 包含ブロック(containing block)
それぞれを順に見ていきます。
スクロールコンテナ:どのスクロールに反応するか
スクロールコンテナは、sticky 要素がどのスクロールに反応するかを決める祖先要素です。
ふつうのページの場合、スクロールコンテナは画面全体(viewport)になります。ただし、overflow プロパティに visible 以外の値(hidden / scroll / auto など)が指定された祖先要素が間にある場合は、その祖先要素がスクロールコンテナになります。
包含ブロック:どこまで動けるか
包含ブロックは、sticky 要素がどこまで貼り付いていられるか(活動範囲)を決める祖先要素です。多くの場合、一つ上の親要素が包含ブロックになります。
これが、position: sticky の動きで一番大事な原則です。sticky 要素は、親要素という枠の中だけで貼り付き、枠を出ると一緒に押し出されていきます。
だからこそ、position: sticky を思い通りに動かすコツは、「sticky 要素自体」だけでなく「親要素(枠)をどう作るか」も一緒に考えることになります。
動きが切り替わる2つのタイミング
スクロールコンテナと包含ブロックの関係を整理すると、sticky 要素の動きは次のように切り替わっていきます。

動きを分解すると、2つのタイミングで切り替わっていることがわかります。
- 貼り付くタイミング(上の図の③→④):
sticky 要素がtop/bottomで指定した位置に達したとき(top: 0ならスクロールコンテナの上端、bottom: 0なら下端) - 離れるタイミング(上の図の⑤→⑥):
包含ブロックの端がスクロールで通り過ぎたとき(top: 0なら包含ブロックの下端、bottom: 0なら上端)
bottom の動きは、ページを下から上にスクロールしているところをイメージすると分かりやすいです。top と上下が反転しているだけで、考え方は同じです。
動きを確認
実際の動きで確認してみましょう。
上のデモは、3つのセクションが上から順に並んだ構成になっています。
- 通常セクション:
position: stickyの指定がない、ふつうのコンテンツ - sticky の親要素:薄い青の背景。この中に青いバー(sticky 要素)が入っている
- 通常セクション:①と同じく、ふつうのコンテンツ
スクロールすると、
- ①では、コンテンツが普通に流れていく
- ②に入ると青いバーが現れ、画面の上端に達した瞬間に貼り付く
- ②の中をスクロールしている間、バーは画面の上端に留まり続ける
- ②の下端まで来ると、バーは押し出されて見えなくなる
- ③では、バーは存在しない(②の外なので)
という流れが確認できます。
コード
sticky を機能させている部分だけを抜き出すと、次のようになります。
<div class="sticky-parent">
<p>...</p>
<div class="sticky-bar">私は sticky 要素(親要素の下端で離れます)</div>
<p>...</p>
</div>.sticky-parent {
/* ... */
}
.sticky-bar {
position: sticky;
top: 0;
/* ... */
}使用例
ここからは、よく使う使用例を見ていきます。それぞれデモとコードをセットで紹介します。
なお、これから紹介する5つの使用例で、それぞれ sticky 要素の「スクロールコンテナ」と「包含ブロック」がどうなっているかを、一覧でまとめておきます。
| 使用例 | スクロールコンテナ | 包含ブロック |
|---|---|---|
| 例1:セクション見出しの貼り付き | 画面全体 | <section> |
| 例2:bottom: 0 で下に貼り付く | 画面全体 | .product |
| 例3:サイドバーの追従 | 画面全体 | .layout-inner |
| 例4:テーブルの thead 固定 | .table-wrapper | <table> |
| 例5:テーブルの一番左の列の固定 | .table-wrapper | <table> |
例1:セクション見出しの貼り付き
長いコンテンツを複数のセクションに区切るとき、各セクションの見出しを position: sticky にすると、スクロールに合わせて見出しが入れ替わる動きを作れます。冒頭のデモで見ていただいたパターンです。
コード
sticky を機能させている部分だけを抜き出すと、次のようになります。
<section class="topic-section">
<div class="topic-head">
<h2 class="topic-title">セクション1:はじめに</h2>
</div>
<div class="topic-content">
<p>...</p>
</div>
</section>
<section class="topic-section">
<div class="topic-head">
<h2 class="topic-title">セクション2:背景</h2>
</div>
<div class="topic-content">
<p>...</p>
</div>
</section>
<!-- ... -->.topic-head {
position: sticky;
top: 0;
/* ... */
}ポイントは、.topic-head の親要素が、各 <section> になっていることです。「動作の仕組み」で見たとおり、sticky 要素は親要素の中だけで貼り付くので、次の <section> に入ると前の見出しが押し出され、新しい見出しが貼り付くようになります。
例2:bottom: 0 で下に貼り付く
top ではなく bottom で位置を指定すると、要素は画面の下端に貼り付きます。ECサイトの「カートに入れる」ボタンや、コーポレートサイトや LP の「お問い合わせ」「申し込む」ボタンなど、長いページでも CTA を常に押せる状態にしておきたいときによく使うパターンです。
コード
sticky を機能させている部分だけを抜き出すと、次のようになります。
<div class="product">
<div class="product-body">
<!-- 商品画像、商品情報、商品説明、特徴、仕様、レビューなど -->
</div>
<div class="product-cta">
<a href="#">カートに入れる</a>
</div>
</div>.product-cta {
position: sticky;
bottom: 0;
/* ... */
}このデモでは、CTA ボタンを商品情報の一番後ろに置いています。こうすることで、商品情報をスクロールしている間はずっと画面下端にボタンが貼り付き、最後まで到達すると本来の位置に収まります。
例3:サイドバーの追従
2カラムレイアウトで、片方のカラム(サイドバー)をスクロールに追従させる使い方です。記事ページの目次や、商品ページの「カートに入れる」エリアなどでよく見るパターンです。
コード
sticky を機能させている部分だけを抜き出すと、次のようになります。
<div class="layout">
<div class="layout-inner">
<div class="layout-main">
<!-- メインコンテンツ -->
</div>
<aside class="layout-side">
<!-- 目次など -->
</aside>
</div>
</div>.layout-inner {
display: flex;
align-items: flex-start; /* sticky を効かせるためのポイント */
/* ... */
}
.layout-side {
position: sticky;
top: 40px;
/* ... */
}flexbox レイアウトでサイドバーを sticky にする場合、align-items: flex-start(または sticky 要素側に align-self: flex-start)を指定するのがポイントです。これを忘れると、サイドバーがメインカラムと同じ高さに自動で引き伸ばされてしまい、sticky が機能しなくなります。
なお、スマホなどの狭い画面では、サイドバーを記事の上や下に配置して sticky を無効化するのが定番です。今回のデモも、767px 以下ではサイドバーが記事の上に表示されます。
例4:テーブルの thead 固定
縦に長い表で、ヘッダー行(<thead>)をスクロール中も常に表示しておきたいときに便利です。データ件数の多い管理画面や、長い比較表などでよく使うパターンです。
ここから、スクロールコンテナと包含ブロックがこれまでの例とは違うパターンが出てきます。スクロールコンテナは画面全体ではなく、縦スクロールを担当するラッパー要素(.table-wrapper)です。包含ブロックも、「2. 仕組み」のアイコンボックスで触れたとおり、テーブル要素の例外として、<table> 自体になります。これは、CSS のテーブルレイアウトでは、<tr> や <thead> 自体は枠(包含ブロック)の役割を持たず、<table> 全体がその役割を担うためです。
| ID | 名前 | 部署 | 役職 | 状態 |
|---|---|---|---|---|
| 001 | 山田 太郎 | エンジニアリング | マネージャー | 在籍中 |
| 002 | 鈴木 花子 | デザイン | リーダー | 在籍中 |
| 003 | 佐藤 健 | マーケティング | シニア | 在籍中 |
| 004 | 田中 美咲 | 営業 | マネージャー | 在籍中 |
| 005 | 高橋 拓海 | エンジニアリング | シニア | 休職中 |
| 006 | 渡辺 由美 | 人事 | リーダー | 在籍中 |
| 007 | 伊藤 航 | エンジニアリング | ジュニア | 在籍中 |
| 008 | 中村 香織 | デザイン | シニア | 在籍中 |
| 009 | 小林 俊介 | 営業 | シニア | 在籍中 |
| 010 | 加藤 梨花 | マーケティング | ジュニア | 在籍中 |
| 011 | 吉田 竜也 | エンジニアリング | リーダー | 在籍中 |
| 012 | 山本 千尋 | 経理 | マネージャー | 在籍中 |
| 013 | 松本 翔 | エンジニアリング | ジュニア | 在籍中 |
| 014 | 井上 彩 | デザイン | ジュニア | 休職中 |
| 015 | 木村 大輔 | 営業 | リーダー | 在籍中 |
| 016 | 林 みなみ | カスタマーサポート | シニア | 在籍中 |
| 017 | 清水 優 | マーケティング | シニア | 在籍中 |
| 018 | 山口 悠 | エンジニアリング | シニア | 在籍中 |
| 019 | 池田 真央 | 人事 | ジュニア | 在籍中 |
| 020 | 阿部 隆 | 経理 | リーダー | 在籍中 |
| 021 | 森 一郎 | 営業 | ジュニア | 在籍中 |
| 022 | 橋本 綾 | デザイン | マネージャー | 在籍中 |
| 023 | 石川 学 | カスタマーサポート | リーダー | 在籍中 |
| 024 | 前田 玲奈 | マーケティング | マネージャー | 在籍中 |
| 025 | 藤田 淳 | エンジニアリング | マネージャー | 在籍中 |
| 026 | 岡田 理沙 | 人事 | シニア | 在籍中 |
| 027 | 後藤 健太 | エンジニアリング | ジュニア | 在籍中 |
| 028 | 近藤 さくら | 営業 | シニア | 休職中 |
| 029 | 遠藤 健一 | カスタマーサポート | ジュニア | 在籍中 |
| 030 | 青木 葵 | デザイン | シニア | 在籍中 |
コード
sticky を機能させている部分だけを抜き出すと、次のようになります。
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>ID</th>
<th>名前</th>
<th>部署</th>
<!-- ... -->
</tr>
</thead>
<tbody>
<tr>
<td>001</td>
<td>山田 太郎</td>
<td>エンジニアリング</td>
<!-- ... -->
</tr>
<!-- ... -->
</tbody>
</table>
</div>.table-wrapper {
max-height: 400px;
overflow-y: auto;
/* ... */
}
.table-wrapper thead th {
position: sticky;
top: 0;
/* ... */
}thead 自体ではなく th(各セル)に position: sticky を指定するのが、互換性の高い書き方です。最近のブラウザは thead への指定にも対応していますが、th に指定すれば古いブラウザでも確実に動作します。
なお、スマホなどの狭い画面では、列が多いとテーブルが収まらないので、今回のデモでは、テーブル自体を viewport より広くして、横にもスクロールできるようにしています。
今回のデモは sticky 要素(thead)がほぼ「ずっと貼り付いた状態」で、自然な位置〜貼り付き〜離れまでの動きはあまり体感できません。ただ、こうした画面全体以外のスクロール領域内に貼り付く動きは position: fixed では作れない、stickyならではの強みです。
例5:テーブルの一番左の列の固定
横に長い表で、行ラベルとなる一番左の列をスクロール中も常に表示しておきたいときに使うパターンです。スプレッドシートの「行見出し固定」のような動きで、列数の多いダッシュボードや、スマホで横スクロールが必要なテーブルでよく使います。
例4と同じく、スクロールコンテナは .table-wrapper(横スクロールを担当)、包含ブロックは <table> です。
| 選手名 | チーム | 試合 | 打席 | 打数 | 打率 | 安打 | 本塁打 | 打点 | 盗塁 | 四球 | 三振 | OPS |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 山田 健太 | 巨人 | 142 | 612 | 540 | .328 | 177 | 14 | 78 | 18 | 58 | 75 | .876 |
| 佐藤 翔 | 阪神 | 140 | 598 | 518 | .315 | 163 | 32 | 105 | 5 | 72 | 115 | .945 |
| 鈴木 大輔 | ソフトバンク | 135 | 588 | 521 | .305 | 159 | 8 | 52 | 35 | 62 | 82 | .815 |
| 田中 涼介 | DeNA | 138 | 580 | 487 | .298 | 145 | 38 | 115 | 2 | 85 | 135 | .985 |
| 高橋 達也 | 日本ハム | 132 | 560 | 502 | .291 | 146 | 28 | 92 | 8 | 52 | 148 | .872 |
| 中村 拓海 | ヤクルト | 141 | 595 | 530 | .287 | 152 | 18 | 78 | 12 | 58 | 98 | .815 |
| 加藤 隆志 | ロッテ | 139 | 590 | 489 | .282 | 138 | 15 | 68 | 15 | 95 | 82 | .848 |
| 吉田 慎吾 | 広島 | 136 | 570 | 518 | .278 | 144 | 10 | 58 | 22 | 48 | 72 | .755 |
| 渡辺 颯太 | 楽天 | 140 | 592 | 498 | .275 | 137 | 17 | 72 | 9 | 88 | 105 | .830 |
| 松本 雄大 | オリックス | 138 | 578 | 511 | .272 | 139 | 24 | 95 | 6 | 60 | 118 | .838 |
コード
sticky を機能させている部分だけを抜き出すと、次のようになります。
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>選手名</th>
<th>チーム</th>
<th>試合</th>
<!-- ... -->
</tr>
</thead>
<tbody>
<tr>
<th>山田 健太</th>
<td>巨人</td>
<td>142</td>
<!-- ... -->
</tr>
<!-- ... -->
</tbody>
</table>
</div>.table-wrapper {
overflow-x: auto;
/* ... */
}
.table-wrapper thead th:first-child,
.table-wrapper tbody th {
position: sticky;
left: 0;
background: #fff; /* 重要:背景色は必須 */
/* ... */
}この例に限らず、sticky を使うときの注意点として、sticky にした要素には背景色を指定するのが基本です。指定しないと、スクロールで後ろにある他の要素が、sticky 要素を通して透けて見えてしまいます。
まとめ
この記事では、position: sticky の基本と、よく使う使用例を解説しました。
ポイントは下記です。
position: stickyは、スクロール位置に応じて要素を貼り付ける CSS プロパティ- 動かすには
top/bottom/left/rightのいずれかの指定が必須 - 動きは「スクロールコンテナ」(どのスクロールに反応するか)と「包含ブロック」(どこまで動けるか)の2つの祖先要素で決まる
- 思い通りに動かすコツは、「sticky 要素そのもの」だけでなく「親要素(枠)をどう作るか」も一緒に考えること
- セクション見出しの貼り付き、bottom: 0 の下貼り付き、サイドバー追従、テーブルのスクロール追従など、JavaScript なしで色々な動きが作れる
position: sticky は CSS だけで完結する手軽さが魅力ですが、実は「指定したのに効かない」「思ったところで貼り付かない」というハマりどころも多いプロパティです。
そのあたりは、後編「position: sticky が効かない原因と対処法(仮)」 で詳しく扱う予定です。あわせてお読みいただければ、position: sticky をひと通り使いこなせるようになるはずです。
押していただけると励みになります!
