パララックスやスクロール連動アニメーションを実装するとき、最初にscrollイベントとJavaScriptを思い浮かべることがあります。コンポーネントの幅に応じてレイアウトを変えるならResizeObserver、ツールチップの位置を決めるならJavaScriptの位置計算やライブラリ、といった具合です。
もちろん、それらの方法が間違いというわけではありません。ただ、現在のCSSには、以前ならJavaScriptへ任せるしかなかった処理をブラウザへ宣言的に伝えられる機能が増えています。
この記事で考えたいのは、JavaScriptを減らすこと自体ではなく、CSSで表現できる見た目やレイアウトまでJavaScriptに任せないことです。
原文は「1行のCSSで20行のJavaScriptを消す」という強い表現で、6つのCSS機能を紹介しています。ここでは内容を日本語の記事として読みやすい形に書き直し、ブラウザ対応状況や性能については2026年8月25日時点の情報に合わせて補足しています。特に、原文にある「すべて主流ブラウザで全面対応」「CSSのスクロールアニメーションなら必ずCompositor Threadだけで動く」といった断定は、そのまま採用していません。
1. :has()で子要素の状態を親要素のスタイルへ反映する
フォームの入力欄へフォーカスしたとき、外側のコンテナも強調したいとします。
以前なら、JavaScriptでfocusとblurを監視し、親要素へクラスを付け外しする実装がよく使われました。
const input = document.querySelector(".form-input");
const wrapper = document.querySelector(".form-wrapper");
input.addEventListener("focus", () => {
wrapper.classList.add("is-focused");
});
input.addEventListener("blur", () => {
wrapper.classList.remove("is-focused");
});
.form-wrapper.is-focused {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgb(59 130 246 / 30%);
}
現在は:has()を使うことで、「内部のinputがフォーカスされている親要素」という条件をCSSだけで表せます。
.form-wrapper:has(input:focus) {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgb(59 130 246 / 30%);
}
この違いは単なるコード行数ではありません。フォーカス状態をJavaScript側へ複製しなくてよくなり、DOMの状態と見た目の関係をCSSの中で完結できます。
:has()はほかにも、次のような用途で使えます。
/* 画像を含むカードだけレイアウトを変える */
.card:has(img) {
grid-template-columns: 160px 1fr;
}
/* 無効な入力を含むフォーム内の送信ボタンを視覚的に弱める */
form:has(:invalid) .submit-button {
opacity: 0.5;
}
ただし、後者はあくまで見た目の変更です。ボタンそのものをdisabled状態へ変更するわけではありません。CSSはアプリケーションの状態変更を代替するものではなく、DOMにすでに存在する状態を条件としてスタイルを変えるために使う、と考えると境界が分かりやすくなります。
:has()はMDN Baselineでは2023年12月から「Widely available」とされており、今回紹介する機能の中では比較的導入しやすい部類です。
2. @containerでコンポーネント自身の幅に応じてレイアウトする
従来の@mediaは、基本的にビューポートを基準にします。しかし、コンポーネント化されたUIでは、同じカードが300px幅のサイドバーに置かれることも、800px幅のメイン領域に置かれることもあります。
このとき必要なのは「画面全体が何pxか」ではなく、「このコンポーネントが今どれだけの幅を与えられているか」です。
JavaScriptで対応するなら、ResizeObserverでコンテナ幅を監視し、クラスを切り替える方法があります。
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
if (width < 400) {
entry.target.classList.add("compact");
entry.target.classList.remove("wide");
} else {
entry.target.classList.add("wide");
entry.target.classList.remove("compact");
}
}
});
observer.observe(document.querySelector(".card-container"));
レイアウトだけが目的なら、Container Queriesで同じ条件をCSSへ移せます。
.card-container {
container-type: inline-size;
}
.card {
display: grid;
grid-template-columns: 1fr;
}
@container (min-width: 400px) {
.card {
grid-template-columns: 200px 1fr;
}
}
これならカードは、自分がサイドバーに置かれているか、メイン領域に置かれているかを知る必要がありません。与えられた領域の幅だけを見て、レイアウトを決められます。
ResizeObserverが不要になること自体よりも、レイアウトの条件をCSSに書けることの方が重要です。
Container QueriesはMDN Baselineで2023年2月から広く利用可能とされており、現在ではコンポーネント設計の選択肢として現実的です。
3. スクロール駆動アニメーションでscrollイベントを減らす
この記事の中でも特に分かりやすいのが、パララックスや読了進捗バーのようなスクロール連動表現です。
JavaScriptで読了進捗バーを実装するなら、たとえば次のようにスクロール位置を計算します。
const progressBar = document.querySelector(".progress-bar");
window.addEventListener("scroll", () => {
const scrollTop = document.documentElement.scrollTop;
const scrollHeight =
document.documentElement.scrollHeight - window.innerHeight;
const progress = scrollTop / scrollHeight;
progressBar.style.transform = `scaleX(${progress})`;
});
この実装では、スクロールイベントが発生するたびにJavaScriptが動きます。実際のプロダクションコードではrequestAnimationFrameを使った更新制御なども検討できますが、単純な「スクロール量とアニメーション進行度を対応させる」という目的に対して、JavaScript側で状態を計算していることに変わりはありません。
Scroll-driven Animationsでは、アニメーションのタイムライン自体をスクロールへ結び付けられます。
.progress-bar {
position: fixed;
inset: 0 auto auto 0;
width: 100%;
height: 3px;
transform: scaleX(0);
transform-origin: left;
background: #3b82f6;
animation: grow-progress linear;
animation-timeline: scroll(root);
}
@keyframes grow-progress {
to {
transform: scaleX(1);
}
}
原文ではwidthを0%から100%へ変化させていますが、この記事ではtransform: scaleX()に変更しています。widthの変更はレイアウトへ影響し得るため、「CSSにしたから自動的に軽い」と考えるのではなく、アニメーションさせるプロパティも意識した方がよいためです。
パララックスも考え方は同じです。スクロール位置をJavaScriptで取得してtransformを毎回更新するのではなく、対応ブラウザであればスクロールタイムラインへアニメーションを結び付けられます。
.parallax-layer {
animation: parallax linear both;
animation-timeline: scroll(root);
}
@keyframes parallax {
from {
transform: translateY(-5vh);
}
to {
transform: translateY(5vh);
}
}
ただし、ここは互換性に注意が必要です。2026年8月25日時点でMDNはanimation-timelineを「Limited availability」としており、Baselineにはなっていません。
そのため、「これからはパララックスをすべてCSSに置き換える」と決め打ちするより、対象ブラウザを確認した上で、対応ブラウザでは演出を有効にし、未対応でも主要機能は壊れないProgressive Enhancementとして使うのが現実的です。
また、CSSのスクロール駆動アニメーションだからといって、すべてのプロパティが必ずCompositor Threadだけで処理され、主スレッドの負荷がゼロになるわけではありません。性能上の利点は期待できますが、何をアニメーションするかによってレンダリングコストは変わります。
4. @starting-styleで表示開始時のトランジションを書く
display: noneだった要素を表示するとき、従来は「表示した直後の初期スタイル」を作るためにJavaScriptでタイミングをずらしたり、強制的にレイアウトを確定させたりする実装がありました。
function showModal(element) {
element.style.display = "block";
// レイアウトを確定させてからクラスを追加する古典的な実装例
element.offsetHeight;
element.classList.add("visible");
}
現在は@starting-styleと離散プロパティのトランジションを組み合わせることで、表示開始時のスタイルをCSS側に記述できます。
.modal {
display: none;
opacity: 0;
transition:
opacity 0.3s ease,
display 0.3s allow-discrete;
}
.modal.open {
display: block;
opacity: 1;
@starting-style {
opacity: 0;
}
}
ここで注意したいのは、@starting-styleが「モーダルを開くというアプリケーションの状態変更」まで担うわけではないことです。
.openを付ける処理が必要なら、その状態管理はJavaScriptの役割です。<dialog>やPopover APIなどHTML側に状態を持てる仕組みを使う場合もあります。CSSが担うのは、表示状態が切り替わったときに、どのように見せるかという部分です。
@starting-styleとtransition-behavior: allow-discreteはMDN Baseline 2024の機能です。新しいブラウザでは利用しやすくなっていますが、古いOSに固定されたブラウザまで対象にする場合は互換性確認が必要です。
5. field-sizing: contentでtextareaを内容に合わせる
入力内容に応じてtextareaの高さを自動調整する処理も、長い間JavaScriptで実装されてきました。
const textarea = document.querySelector("textarea");
textarea.addEventListener("input", () => {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
});
対応ブラウザでは、field-sizing: contentで同じ目的をCSSへ移せます。
textarea {
field-sizing: content;
}
実際には無制限に伸びると使いにくいため、最小・最大サイズも併せて決めておくと扱いやすくなります。
textarea {
field-sizing: content;
min-block-size: 4lh;
max-block-size: 12lh;
}
これは「入力イベントに反応して高さを計算する」という処理自体が不要になる例です。フォームの値を扱うJavaScriptは残っても、入力欄の見た目を維持するためだけのイベントリスナーは減らせます。
field-sizingはMDN Baseline 2026で、2026年6月から主要ブラウザの最新世代で利用可能とされています。かなり新しいため、利用者に古いブラウザが多いサービスではフォールバックを確認してから採用する方が安全です。
6. CSS Anchor Positioningでツールチップの位置を決める
ボタンの近くへツールチップやポップオーバーを表示するとき、位置計算には意外と多くの条件があります。基準となる要素の位置、スクロール、画面端からのはみ出し、上下左右の切り替えなどです。
そのため、Popper.jsやFloating UIのようなライブラリが使われてきました。
CSS Anchor Positioningでは、基準となる要素をアンカーとして宣言し、別の要素をその位置へ関連付けられます。
.trigger-button {
anchor-name: --tooltip-anchor;
}
.tooltip {
position: fixed;
position-anchor: --tooltip-anchor;
top: anchor(bottom);
left: anchor(center);
translate: -50% 8px;
position-try-fallbacks: flip-block, flip-inline;
}
これにより、単純な位置関係のためにgetBoundingClientRect()を呼び、座標をJavaScriptで同期する必要がない場面が増えます。
ただし、ここでも「ライブラリを必ず削除できる」とまでは言えません。Floating UIのようなライブラリは位置決め以外の細かな制御や互換性も担っています。必要な機能がCSS Anchor Positioningの範囲に収まるなら依存を減らせる、という判断が適切です。
CSS Anchor Positioningの関連機能は2026年にBaseline入りしたものもありますが、比較的新しい機能です。特に既存サービスへ導入するときは、実際に使うプロパティ単位で互換性を確認する必要があります。
6つの機能でブラウザ対応状況は異なる
原文では6機能をまとめて「2026年には主流ブラウザで全面対応」としています。しかし実際には、すぐ採用しやすいものと、対象ブラウザを慎重に確認した方がよいものがあります。
2026年8月25日時点のMDN Baselineを基準に、実務でどこまで使いやすいかを見ると次のようになります。
| 機能 | 状況の目安 | 実務での考え方 |
|---|---|---|
:has() | Widely available | 通常のWebサイトでも採用しやすい |
| Container Queries | Widely available | コンポーネント単位のレスポンシブで採用しやすい |
| Scroll-driven Animations | Limited availability | 対象ブラウザ確認かProgressive Enhancementが必要 |
@starting-style / transition-behavior | Baseline 2024 | 古いブラウザを含む場合は確認する |
field-sizing | Baseline 2026 | 新しいためフォールバックを意識する |
| CSS Anchor Positioning | 2026年にBaseline入りした関連機能あり | 使用するプロパティ単位で確認する |
「CSSで書ける」という仕様上の可否と、「自分のプロダクトで今日使える」という判断は別です。CSSに置き換えること自体を目的にせず、サポート対象とフォールバックにかかる手間まで含めて決める必要があります。
本質は「JavaScriptを書かない」ではなく、役割に合わせて使い分けること
今回の6例を見ていると、共通点があります。
JavaScriptが不要になりやすいのは、次のような処理です。
- DOMにすでに存在する状態を見て、見た目を変える
- コンテナの大きさに応じてレイアウトを変える
- スクロール量と視覚効果を対応させる
- 表示開始・終了時のトランジションを定義する
- フォーム部品の見た目を内容に合わせる
- ある要素を基準に別の要素を配置する
いずれも、データ取得やビジネスロジックではなく、ブラウザのレイアウトや描画と強く結び付いた処理です。
一方で、JavaScriptが必要な領域は当然残ります。API通信、データ変換、アプリケーション状態、認証、複雑なユーザー操作、サーバーとの同期などをCSSへ押し込むことはできません。
そのため実装時は、次の順序で確認するとJavaScriptが必要かどうかを判断しやすくなります。
- これはアプリケーションの状態やロジックか、それとも見た目・レイアウトの問題か
- ブラウザ標準のHTML/CSSだけで宣言できるか
- 対象ブラウザで十分に利用できるか
- 未対応時のフォールバックはどこまで必要か
- それでもJavaScriptが必要なら、必要な部分だけをJavaScriptへ任せる
なんでもJavaScriptで実装するのではなく、CSSでできることはCSSでやる。
これは「JavaScriptの行数を減らせば優れている」という話ではありません。状態の二重管理や不要なイベント監視を減らし、見た目とレイアウトはCSS、アプリケーションの状態やロジックはJavaScriptというように、処理を適切な場所へ置くための判断です。
参考資料
- MDN,
:has() - MDN, CSS container queries
- MDN,
animation-timeline - MDN,
@starting-style - MDN,
transition-behavior - MDN,
field-sizing - MDN, CSS anchor positioning