なんでLenisを使うときにbodyを殺して、謎のラッパー構造を作るのか?
Lenis を好んで使用する理由
当方がLenisを好んで使用する理由は、スクロールを単なるブラウザ挙動ではなく、制御可能な状態として扱える点にある。仮想スクロール値を基準に transform ベースで描画が統一されることで、座標系と時間軸を一貫して管理できる。この時点で、すでに「ただのスクロール」ではなくなっている。
カメラを固定し、世界を動かす——そう捉えた瞬間、スクロールはUIではなく演出へと変質する。パララックスや各種アニメーションも、同一の進行度に束ねられ、ズレなく同期する。
……と、このあたりを書き始めるとそのまま結論に到達してしまうので、詳細は後述することとする。
基本構造(HTML)
<body><div id="lenis-root">
<main>
<section id="hoge">
<div class="parallax-bg"></div>
<p>hogehoge</p>
</section>
<section class="others"><p>hogehoge01</p></section><!-- other -->
<section class="others"><p>hogehoge02</p></section><!-- other -->
<section class="others"><p>hogehoge03</p></section><!-- other -->
</main>
</div></body>
基本スタイル(CSS)
@import url('./master.css');
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body { overflow: hidden; }
#lenis-root { height: 100%; }
#lenis-root { position: fixed; inset: 0; overflow: hidden; }
#lenis-root > main { will-change: transform;}
#hoge { position: relative; min-height: 100vh; overflow: hidden; }
.parallax-bg {
position: absolute; inset: -20%;
background-size: cover; background-position: center;
transform: translateY(0); will-change: transform;
}
/* 動作テスト用 */
.others { min-height: 100vh; display: flex; justify-content: center; align-items: center; background-color: #f0f0f0; font-size: 32px; color: #333; }
結論
Lenis は「スクロール」を使っていない。
「スクロールっぽい錯覚」をtransformで作っている。
これがすべて。
まず前提:ブラウザ標準のスクロールとは何か
[ viewport(画面) ]
↓ スクロール
[ body(ドキュメント) ]
通常の Web ページはこうなっている。つまり…
- bodyの高さがコンテンツ量になる
- スクロールすると ブラウザが勝手に body を上下にずらす
- JS は「結果」をscrollYとして受け取るだけ
しかしLenisは設計上、この前提を壊す。
大変面白い知見を得ることができた。
Lenis は「ぬるっとスクロールさせるライブラリ」ではない
見出しの通り。Lenisは「ぬるっとスクロールさせるライブラリ」ではない。
正確には「スクロールを<演出>として再実装するライブラリ」である。
Lenisが考えている事は
- 本物のスクロール:使わない
- 人間には「スクロールしているように見せる」
- 自分で位置を計算する
- transform: translateY()で世界を動かす
だから body を殺す
html, body { height: 100%; }
body { overflow: hidden; }
これは単なるスタイルではなく、システム設計。
- ネイティブスクロールの無効化
- scrollY の破棄
- 入力(ホイール/タッチ)のみ取得
これは「スクロールを止めるため」のCSS
Lenis の構造:固定カメラモデル
[ 固定されたカメラ (#lenis-root) ]
↓
[ 動く世界 (main) ]
正直なところ、この仕様には最初は頭を悩ませた。そこで筆者は、アニメーションの撮影工程的な捉え方を導入することにした。かつてのセルアニメーションにおける撮影では、背景美術の上にセル画を重ね、多層のレイヤーをカメラで撮影することで画面を構成していた。カメラ自体は基本的に固定されており、移動や奥行きの表現は、セルや背景をスライドさせることで実現される。いわゆるマルチプレーン撮影においては、レイヤーごとに異なる速度で移動させることで視差を生み、擬似的な奥行きを表現していた。
あいにく筆者が生まれた頃には、こうした撮影工程の多くはすでにデジタルへと移行していたようだが、この「カメラは固定され、画面側が動く」という発想自体は現在のデジタルコンポジットにおいても本質的には変わっていない。
Lenisの挙動もこれに近い。すなわち、視点(#lenis-root)は固定されたまま、シーン全体(main)が移動する。スクロールしているのではなく、撮影対象そのものが流れていく——そう解釈した方が実態に近い。見た目上は確かに、「自分がスクロールしている」と感じる。しかし、実際はカメラは固定であり、撮影台そのものが上に動いているイメージで考えた。ゲームエンジンで言うと、カメラを動かしているのではなくステージを逆方向に動かしているイメージになるのだろうか。
実際のDOM構造
<body>
<div id="lenis-root">
<main>
<!-- 全コンテンツ -->
</main>
</div>
</body>
CSSの意味を分解する
#lenis-root { position: fixed; inset: 0; overflow: hidden; }
これはつまり…
<画面いっぱい><絶対に動かない><常にviewportと一致>
それに対してカメラが、
#lenis-root > main { will-change: transform; }
ここに Lenis がtransform: translateY(-xxxpx)を当てることにより、実際に動くのはmainだけ。
なぜこの構造じゃないとLenisは成立しないのか
Lenis の処理フロー:
- 入力(ホイール・タッチ)を受け取る
- 仮想的なscroll positionを計算する
- イージング適用
- transformに反映
ここでbodyネイティブスクロールが存在すると:
- ブラウザも動く
- Lenis も動く
- 二重スクロールになる
…パララックスは壊れる。 getBoundingClientRect()の基準もズレる。
だからbodyを殺す必要がある。
パララックスと完全に相性がいい理由
この構造の本質はここ。
- 全てがtransform ベース
- 同一座標系
- 同一時間軸(進行度)
つまり:
- Lenis進行度
- 仮想スクロール位置
- 要素位置
- getBoundingClientRect依存
- パララックス量
- 任意の補間
これらを 完全に同期できる。
雑なまとめ
- Lenis はスクロールライブラリではない
- 仮想スクロールエンジン
- bodyスクロールは完全排除
- カメラ固定 / 世界移動モデル
- transformを統一的に制御する設計
本稿は、実装および挙動観察に基づく整理であり、公式仕様を網羅的に解説するものではない。したがって、その記述は必ずしも公式の定義と一致するものではない点を、あらかじめ断っておく。記述には一定の仮定と解釈を含むが、スクロールという現象をどのように捉え、いかに設計対象として扱うか——その視点を補助する一助となれば幸いである。