スケートボードでトリックを決めるとき、足元の板と体の動きが完全に連動していないと成立しない。Webのスクロール体験にも同じことが言える。ユーザーの指の動き(スクロール)と画面の応答が噛み合って初めて、直感的な操作感が生まれる。
京谷商会のトップページには18の専門ポータルが並んでいて、それぞれのセクションに色付きのバーがある。このバーが画面上端に達すると、そのままヘッダーとして固定される。次のバーが来れば、前のバーを押し出して入れ替わる。やっていることはシンプルだけど、ユーザーに「今どこにいるか」を常に伝えるUIとして、かなり効いている。
以下のデモでは、実際にスクロールしてバーの入れ替わりを体感できる。各セクションのカラーバーが画面上端で固定され、次のバーが到達すると押し出されて入れ替わる動作を確認してほしい。
スティッキーセクションバーの動作デモ。iframe内でスクロールしてバーの入れ替わりを体感できる。
position: sticky だけで成立する仕組み
正直に言うと、最初はJavaScriptでスクロール位置を監視して切り替える方向で考えていた。でも実際にやってみたら、CSSの position: sticky; top: 0 だけで全部解決した。
stickyの優れているところは、親要素の範囲内でのみ固定されるという制約にある。セクションバーに sticky を指定すると、そのセクションが画面内にある間だけバーが上端に留まって、セクションがスクロールアウトすれば一緒に消える。JavaScriptゼロで「入れ替え」が成立する。CSS-Tricksのsticky解説を読んだときに「これだ」と思った。
W3CのCSS Positioned Layout仕様では、stickyを「制約付き固定配置」と定義している。通常のフローに参加しつつ、スクロールコンテナ内の指定位置に「引っかかる」この挙動は、W3C CSS Positioned Layout Module Level 3の仕様書に正式に定義されている。2026年現在、主要ブラウザの対応率は98%を超えており、プレフィックスなしで安心して使える。
ただしハマりポイントがある。祖先要素に overflow: hidden がかかっていると、stickyが効かなくなる。コーヒーの焙煎で温度管理を怠ると全部台無しになるのと同じで、CSSの基盤部分の設計がずれていると上に何を積んでもうまくいかない。もう一つ注意が必要なのは、stickyの親要素自体に十分な高さがないケースだ。親のコンテンツ領域がstickyな子とほぼ同じ高さしかないと、固定される余地がないため実質的に効果がない。
z-indexのレイヤーマップ
このUIで一番神経を使ったのがレイヤーの重なり順だ。3つの要素が独立した固定位置を持つため、z-indexの設計を間違えると見た目が破綻する。
僕らが採用したのはこういうレイヤーマップになる。最下層にヘッダー(z-index: 1000)、その上にセクションバー(z-index: 1001)、最上層にナビゲーションメニュー(z-index: 1002)。セクションバーがヘッダーより上にあるのは、バーがヘッダーの上を「滑って」入れ替わる動きを自然に見せるため。ナビは何があっても触れる場所に置きたいから、一番上に独立させた。
下のセクションバーが上のバーを覆い隠す動きは、DOMの描画順序に任せている。MDNのスタッキングコンテキスト解説で詳しく説明されている通り、同じz-indexの要素はHTMLの出現順で後のものが手前に描画される。つまりHTMLで後に書かれたセクションが自動的に前のセクションの上に来る。特別な処理は何も書いていない。スタッキングコンテキストを正しく理解していれば、z-indexの値を細かく変える必要がないことがわかる。
レイヤー管理で重要なのは、z-indexの値をバラバラに付けるのではなく、プロジェクト全体でデザインシステムとしてレイヤーマップを定義しておくことだ。京谷商会では「ベースコンテンツ: 0〜99」「固定ヘッダー: 1000」「ナビゲーション: 1001〜1002」「モーダル: 2000」「トースト通知: 3000」のように用途別にレンジを切り分けている。
ブランドカラーの18色設計
18ポータルそれぞれに固有のブランドカラーが割り当てられていて、セクションバーの背景色になっている。SEOの深い紺、ADSの赤、DEVの緑。色を見るだけでどのポータルかわかるようにしたかった。
ただ18色もあると、明るい色のバーで白文字が読めなくなるケースが出てくる。そこでコントラスト比4.6:1を下回る色は自動的に暗く補正する仕組みを入れた。この閾値はWCAGのAA基準(通常テキスト4.5:1)を少し上回る値で設定している。WCAG 2.2のコントラスト要件(Success Criterion 1.4.3)では、大きいテキスト(18pt以上または太字14pt以上)なら3:1、通常テキストなら4.5:1が最低基準だ。セクションバーのテキストサイズが可変になり得るため、余裕を持って4.6:1とした。
Material Designのカラーロールで語られる「コンテナカラーとオンカラーの関係」も参考にしている。コンテナ(背景色)が変わるなら、その上に載るテキストの色も動的に決定されるべきだ。アクセシビリティとブランドカラーの両立は、妥協ではなく設計の精度で解決する問題だと思う。
ナビゲーションを分離した理由
最初の設計では、セクションバーの中にメニューリンク(サービス・会社概要・お知らせ・お問い合わせ)も入れていた。でもバーが入れ替わるたびにメニューが一瞬ちらつく問題が出て、すぐに別の方法を探した。
結果として、ナビゲーションをセクションバーから完全に分離し、z-index: 1002の独立した固定要素にした。画面右上に常駐しているから、どのセクションを見ていても即座にページ遷移できる。見た目の統合感は少し犠牲になるけど、「いつでもメニューにアクセスできる」というユーザビリティの方が重要だと判断した。3Dモデリングでも、見た目の美しさとメッシュの扱いやすさのどちらを優先するかは常にトレードオフで、機能性を取る場面は多い。
モバイル環境でのsticky挙動
デスクトップでは完璧に動くstickyも、モバイルでは追加の考慮が必要になる。
まず、iOSのSafariではアドレスバーの表示・非表示でビューポートの高さが変わる。top: 0 でstickyを指定していても、アドレスバーが収縮するタイミングでバーの位置がガクッとずれて見えることがある。これに対しては dvh(dynamic viewport height)単位を組み合わせることで、ビューポート変動に追従させる方法が有効だ。web.devのビューポート単位の解説では、svh・lvh・dvh の違いと使い分けが具体的に示されている。
もう一つ、モバイルではstickyバーがスクリーンの縦幅を圧迫しやすい。京谷商会ではモバイル時にセクションバーの高さを48pxから36pxに縮小し、フォントサイズも14pxから12pxに落としている。小さな変更だけど、コンテンツ領域の可読性を確保するうえで無視できない差になる。
Scroll-Driven Animationsとの接続
2026年にはCSS Scroll-Driven Animationsのブラウザ対応が進み、stickyとの組み合わせで新しい表現が可能になっている。
animation-timeline: scroll() を使えば、スクロール位置に応じてCSSアニメーションを駆動できる。これにより、セクションバーが画面上端に到達する瞬間に背景色をフェードさせたり、テキストのスケールを微妙に変えたりといった演出をJavaScript無しで実現できる。従来はIntersectionObserverでスクロール位置を検知してクラスを切り替える必要があったが、CSSだけで完結する選択肢が加わった。
京谷商会のトップページではまだ導入していないが、次のリニューアルでセクションバーのカラー遷移にこの技術を組み込む検討を進めている。stickyの「位置の固定」とscroll-driven animationsの「動きの連動」を掛け合わせることで、よりリッチなスクロール体験を構築できる。
タイルフリップとの連続体験
ページを開くと最初に目に入るのは、河野さんが設計したタイルフリップアニメーションのヒーローだ。世界中の風景写真がタイルの回転で切り替わるあの表現から、スクロールするとカラーバーが次々と入れ替わるスティッキーセクションバーへ。
この2つは別の技術で実装されているけど、訪問者が受け取る体験としては「動きのあるサイト」という一つの印象に統合される。タイルの「回転」とバーの「入れ替わり」。どちらも画面の一部が変化することで文脈の切り替えを伝えるという共通の設計原則に基づいている。
フロントエンドの実装面に興味がある方は、石井が書いたCSS 3D Transformsの実装解説を読んでほしい。CSS 3D Transformsの perspective や backface-visibility の具体的なコードレベルの話が丁寧にまとまっている。