🫑tech.miyabitti
Markdownmdで開く
•

モダンHTMLとCSSはJS無しでどこまでリッチなUIが組めるのか調べたら凄かった

JavaScriptゼロでモーダル、ツールチップ、スクロール連動、排他アコーディオンまで実現!モダンHTML/CSSの劇的進化がすごい

目次

最近のWeb開発といえば

今新しくサイトやWebアプリを立ち上げようとしたら、間違いなくUIライブラリやフレームワークを使うでしょう。
実際筆者も、初歩の初歩的なHTML/CSS/JSから入り、その後すぐにNext.js, Tailwind CSS, shadcn/ui といったスタックでサイトを作っていました。(ぶっちゃけ、まともに document.getElementById や addEventListener を手書きした記憶がほとんどありません)

その後、SSR や SSG といったレンダリングの変遷を追う中でAstro に出会い、 ゼロJS(Zero-JS by default) という思想に大きな衝撃を受けました。(当サイトもAstro製です)

また、昨今のWeb標準の進化を追っていると「最近のCSSは半分ぐらいJS化している」「HTML属性だけでモーダルが作れるようになった」という話を耳にする機会が増えました。

そこで湧いてきたのが、なら、本当にJavaScriptを1行も書かずにどこまでリッチなUIが組めるのか? という純粋な疑問です。

実際に調べてみたところ、タイトルが驚き屋になってしまうくらいネイティブ機能が進化していて凄かったので、最近の便利そうなものを共有していきたいと思います。

HTML標準機能の進化

かつては数十行のJavaScriptを書いていたUIパターンが、今やHTMLタグと属性だけで完結します。

Popover API: 属性だけで作れるツールチップとポップアップ

ツールチップやドロップダウンメニュー、トースト通知などを実装するとき、かつては以下のような処理を手書きしたり、ライブラリに頼り切っていました。

  • z-index の階層管理
  • 要素の外側をクリックしたときに閉じる(Light Dismiss)
  • ESC キーを押したときに閉じる
  • スクリーンリーダー対応や初期フォーカスの移動

これらすべてを、ブラウザがネイティブ処理してくれます。

<!-- トリガーボタン -->
<button popovertarget="my-popover">メニューを開く</button>
<!-- ポップオーバー要素 -->
<div id="my-popover" popover>
<p>これだけで外側クリックやESCキーで閉じられます!</p>
</div>

Tip

popover 属性が付いた要素は、ブラウザの 「Top Layer(最前面レイヤー)」 に描画されるため、親要素の overflow: hidden や z-index に遮られる心配がありません。
※なお、Popover APIは背景を不活性化しない「ノンモーダル(Non-modal)」なUIです。背景の操作をブロックする真のモーダルダイアログを作りたい場合は、<dialog> 要素と .showModal() を組み合わせるか(JSが必要)、今後のHTML仕様(command / commandfor 等)に注目です。

<details name="...">: 排他アコーディオン

FAQセクションなどでよくある「1つ開くと他のアコーディオンが閉じる」アコーディオンUI。

<details> や <summary> タグは以前からありましたが、これまで <details> 要素単体では排他制御ができず、JavaScriptでクリックイベントを拾って他の要素の open 属性を外す必要がありました。

しかし、モダンブラウザでは <details> に name 属性 を付与するだけで排他制御が可能になりました。

<details name="faq">
<summary>Q1: JavaScriptは本当に不要ですか?</summary>
<p>UIの表示切り替えやアニメーションであればHTML/CSSだけで完結します。</p>
</details>
<details name="faq">
<summary>Q2: 古いブラウザの対応はどうすればいいですか?</summary>
<p>Polyfillを利用するか、プログレッシブエンハンスメントとして提供します。</p>
</details>

同じ name="faq" を持たせるだけで、ブラウザが勝手にグループ化して排他オープンしてくれます。

CSSの進化(?)JS化(?):モダンCSS

HTMLだけでなく、CSSの進化も著しいです。かつて「JS必須」と言われていたUIの振る舞いが、CSSプロパティ数行で書けるようになってきています。

:has() 擬似クラス: 親セレクタと状態管理

親要素を選択できる :has() 擬似クラスが主要ブラウザで使えるようになったことで、CSSだけで状態に応じたUI切り替えがかなり柔軟にできるようになりました。

/* ポップオーバーが開いている間、背景のスクロールを固定する */
body:has([popover]:popover-open) {
overflow: hidden;
}
/* チェックボックスの状態に応じて親カードのデザインを変える */
.card:has(input[type="checkbox"]:checked) {
border-color: var(--color-primary);
background-color: var(--color-primary-subtle);
}

子要素のチェック状態やフォーカス状態(:focus-within)を起点にして、親要素やページ全体(body)のスタイルを直接制御できます。

Scroll-driven Animations: スクロール連動をハイパフォーマンスに実行

ページ上部の読了プログレスバーや、スクロールに応じたヘッダーの縮小、セクションのフェードイン。
これらを実装するために window.addEventListener('scroll') や IntersectionObserver を駆使していた処理も、CSSだけで宣言的に記述できます。

/* 読了率スクロールバー */
.progress-bar {
scale: 0 1;
transform-origin: left;
animation: grow-progress linear;
animation-timeline: scroll();
}
@keyframes grow-progress {
to { scale: 1 1; }
}

Note

animation-timeline はブラウザの コンポジタスレッド(Compositor Thread) で処理されるため、メインスレッドのJS実行にブロックされず、滑らかにアニメーションします。

コンポジタスレッドの解説

Tip

コンポジタスレッド(Compositor Thread)とは?

ブラウザの内部処理は、主に 「メインスレッド」 と 「コンポジタスレッド」 に分かれています。

  • メインスレッド: JavaScriptの実行、HTML/CSSのパース、スタイル計算、レイアウト(リフロー)、ペイントなど、Webページの大部分の処理を一手に引き受けています。重いJavaScriptが動いているとメインスレッドが塞がり、スクロールやアニメーションがカクつく原因(Jank)になります。
  • コンポジタスレッド: メインスレッドから渡された描画レイヤー(Layer)をGPUと連携して合成し、画面に出力する独立したスレッドです。

従来のJavaScriptによるスクロール連動(window.addEventListener('scroll') 等)はメインスレッドで実行されるため、メインスレッドが忙しいとカクつきが発生します。一方、animation-timeline: scroll() などのScroll-driven Animationsは、メインスレッドを介さずコンポジタスレッドとGPUだけで直接アニメーションを更新できるため、JavaScriptの実行負荷に左右されず滑らかな描画を維持できます。

Note

※Firefoxではまだ標準有効化されていないため、現時点の実務ではプログレッシブエンハンスメント(対応ブラウザでのみ滑らかに動く装飾)としての活用が安全です。

CSS Anchor Positioning: 要素追従と位置補正

ツールチップやドロップダウンメニューを特定要素の真下に配置し、画面端で見切れる場合は自動で反転(フリップ)させる。
これまで Popper.js や Floating UI などの計算ライブラリに頼りがちだったアンカー追従が、CSS Anchor Positioning でネイティブ対応しつつあります。

/* アンカー(基準となるボタン) */
.trigger-btn {
anchor-name: --menu-trigger;
}
/* ドロップダウンメニュー */
.dropdown-menu {
position: fixed;
position-anchor: --menu-trigger;
position-area: bottom center; /* アンカーの真下に自動配置 */
position-try-fallbacks: flip-block; /* 画面下端にはみ出る場合は自動で上に反転 */
}

Note

※クロスブラウザ対応が必要な本番環境では、OddBird社製の Polyfill を併用するか、段階的な導入を検討してください。

@starting-style & allow-discrete: display: none からのアニメーション

「display: none から display: block に切り替えるときにCSSトランジションが効かない」問題。フロントエンドを触ったことがある人なら、一度は頭を抱えた経験があるはずです。

@starting-style と transition-behavior: allow-discrete を使うことで、要素が表示された直後(最初のフレーム)のスタイルをCSSで明示的に定義できるようになりました。

[popover] {
opacity: 0;
transform: translateY(-10px);
/* overlay と display を allow-discrete でトランジション可能にする */
transition:
opacity 0.3s ease,
transform 0.3s ease,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
/* ポップオーバーが開いた状態 */
[popover]:popover-open {
opacity: 1;
transform: translateY(0);
}
/* 表示された最初のフレームのスタイル */
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: translateY(-10px);
}
}

Tip

Top Layer要素(Popoverやdialog)を閉じる際のアニメーションには、transition に overlay 0.3s allow-discrete を含めるのがポイントです。これがないと、閉じた瞬間に要素がTop Layerから即座に外れてしまい、非表示アニメーションが途中で途切れる現象が起きます。

field-sizing: content: <textarea> の自動リサイズ

ユーザーの入力行数に合わせて自動で伸びるテキストエリアも、field-sizing: content を指定するだけで完結します。scrollHeight を監視して height を書き換えるお決まりのスクリプトはもう不要です。

textarea.auto-grow {
field-sizing: content;
min-height: 4lh; /* 最小4行分 */
}

早速デモページを作ってみた

ここまで紹介した機能を組み合わせて、当サイトの既存CSS・JSを一切読み込まない完全No-JSの架空LPデモをGeminiに作成してもらいました。

デモページで採用した機能一覧
  • スクロールプログレスバー & ヘッダー縮小: animation-timeline: scroll()
  • スクロール連動フェードイン: animation-timeline: view()
  • アンカー追従ドロップダウン: anchor-name + position-area
  • Top Layer ポップアップ & トースト: Popover API + @starting-style
  • 排他アコーディオンFAQ: <details name="faq">
  • オートリサイズお問い合わせフォーム: field-sizing: content + ネイティブバリデーション
  • ダークモード切り替え: <input type="checkbox"> + :has(:checked) + light-dark()

👉 デモページはこちら(完全No-JS体験ページ)

※ブラウザの開発者ツールで JavaScript を無効化(Disable JavaScript)した状態でも、全く同じようにすべてのUIが動作します。

脱線: 調査中に出会った変態技術

TypeScriptの界隈では、型システムがチューリング完全であることを利用して「型だけでオセロを実装した」「型だけでSQLエンジンを作った」といった、型パズルで遊ぶ変態エンジニア(最大級の褒め言葉)がいることは知っていました。

「コンパイル時に型チェックするだけでゲームが動くのすごいな〜」くらいに思っていたのですが、今回のNo-JS調査をしていたら、なんとCSSの世界でも全く同じように変なことをしている人たちがたくさん見つかりました。

CSSだけでゲームや電卓を作る人たち

JavaScriptを使わずにどうやってロジックや状態遷移を組むのかというと、古典的な 「Checkbox Hack」 から最新のCSSプロパティまで、さまざまなアプローチが取られています。

  1. <input type="checkbox"> や <input type="radio"> の :checked 状態、または :hover:active を 「フラグ・入力トリガー」 に見立てる
  2. <label for="..."> や <details> のクリックで 「状態遷移(State Transition)」 を起こす
  3. :has() や兄弟セレクタ(~)、sibling-index() を組み合わせて 「判定ロジック」 を作る
  4. counter-reset や counter-increment を駆使して 「数値計算・値の伝搬」 を行う

これらの仕組みを駆使して作られた、驚くべき作品たちと解説記事がこちらです。

  • まさにTS型オセロのCSS版。ラジオボタンによるCheckbox Hackすら使わず、div:hover:active、sibling-index()、カウンター機能、さらには animation-timeline: view() まで組み合わせて盤面の挟み込み判定をCSSだけで記述しています。
  • 8つの <details> の開閉状態(0/1)と sibling-index()、CSSカウンターを連動させ、二進数の入力値を純CSSでリアルタイム計算・表示している解説記事。
  • ラジオボタンやCSS変数、カウンターを駆使して、エレベーターの階層移動と状態管理を純CSSで実装した詳細な解説記事。

「ブラウザがCSSを解釈できるならなんでも作れるはず」という発想は、TSの型パズルと完全に同じ情熱を感じます。実用性とは別のベクトルですが、言語やツールの仕様の限界に挑むハッカー精神は見ていて本当に刺激的ですね。

まとめ: 「とりあえずJSで書く」を見直してみる

モダンHTMLとCSSの進化を追ってみて、勉強当時に参考にしていた本や記事がジジイ化しつつあるようで少しショックでしたが、素晴らしい機能の数々が日々提案や実装されていて圧倒されました。

複雑な業務ロジックや高度な状態管理には今でもJavaScriptが不可欠です。しかし、UIの表示制御やアニメーション、ちょっとしたインタラクションであれば、ネイティブ機能を使うことで様々なメリットがあります。

  • バンドルサイズ0KB & 描画パフォーマンス:
    余計なスクリプトを読み込まない軽快さに加え、スクロール連動アニメーションなどはコンポジタスレッドでGPU直結で処理されます。
  • 手軽に手に入るアクセシビリティ:
    Popover APIや <details> などは、キーボード操作やESCキーでの破棄、フォーカス管理がブラウザ標準で組み込まれています。
  • フレームワーク非依存の長期的な保守性:
    Web標準に従っているため、フレームワークの乗り換えやバージョンアップの荒波に巻き込まれません。

「UIを作る=まずライブラリを入れる」という固定観念を一度捨て、まずはWeb標準でどこまでできるか? を検討してみると、より軽量で堅牢なWebアプリケーションが作れるのかもしれません。今後もWeb標準の動向を積極的にキャッチアップしていきたいと思います。

参考リンク・リソース