---
title: "モダンHTMLとCSSはJS無しでどこまでリッチなUIが組めるのか調べたら凄かった"
description: "JavaScriptゼロでモーダル、ツールチップ、スクロール連動、排他アコーディオンまで実現！モダンHTML/CSSの劇的進化がすごい"
publishedAt: "2026-09-20T00:00:00.000Z"
tags: ["HTML","CSS","Web"]
---
import { BaselineStatus } from 'astro-embed';

## 最近のWeb開発といえば

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

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

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

そこで湧いてきたのが、:mark[なら、本当にJavaScriptを1行も書かずにどこまでリッチなUIが組めるのか？]{color="blue"} という純粋な疑問です。

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

## HTML標準機能の進化

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

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

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

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

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

```html
<!-- トリガーボタン -->
<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` 等）に注目です。

<BaselineStatus id="popover" />

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

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

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

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

```html
<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"` を持たせるだけで、ブラウザが勝手にグループ化して排他オープンしてくれます。

<BaselineStatus id="details-name" />

## CSSの進化(?)JS化(?)：モダンCSS

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

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

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

```css
/* ポップオーバーが開いている間、背景のスクロールを固定する */
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`）のスタイルを直接制御できます。

<BaselineStatus id="has" />

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

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

```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実行にブロックされず、滑らかにアニメーションします。

:::details[コンポジタスレッドの解説]
> [!TIP]
> **コンポジタスレッド（Compositor Thread）とは？**
>
> ブラウザの内部処理は、主に **「メインスレッド」** と **「コンポジタスレッド」** に分かれています。
>
> - **メインスレッド**: JavaScriptの実行、HTML/CSSのパース、スタイル計算、レイアウト（リフロー）、ペイントなど、Webページの大部分の処理を一手に引き受けています。重いJavaScriptが動いているとメインスレッドが塞がり、スクロールやアニメーションがカクつく原因（Jank）になります。
> - **コンポジタスレッド**: メインスレッドから渡された描画レイヤー（Layer）をGPUと連携して合成し、画面に出力する独立したスレッドです。
>
> 従来のJavaScriptによるスクロール連動（`window.addEventListener('scroll')` 等）はメインスレッドで実行されるため、メインスレッドが忙しいとカクつきが発生します。一方、`animation-timeline: scroll()` などのScroll-driven Animationsは、**メインスレッドを介さずコンポジタスレッドとGPUだけで直接アニメーションを更新できる**ため、JavaScriptの実行負荷に左右されず滑らかな描画を維持できます。
>
> - [Chrome for Developers: Inside look at modern web browser (part 3)](https://developer.chrome.com/blog/inside-browser-part3/)
> - [web.dev: Stick to Compositor-Only Properties and Manage Layer Count](https://web.dev/articles/stick-to-compositor-only-properties-and-manage-layer-count)
:::

<BaselineStatus id="scroll-driven-animations" />

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

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

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

```css
/* アンカー（基準となるボタン） */
.trigger-btn {
  anchor-name: --menu-trigger;
}

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

<BaselineStatus id="anchor-positioning" />

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

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

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

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

```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から即座に外れてしまい、非表示アニメーションが途中で途切れる現象が起きます。

<BaselineStatus id="starting-style" />

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

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

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

<BaselineStatus id="field-sizing" />

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

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

:::details[デモページで採用した機能一覧]
- **スクロールプログレスバー & ヘッダー縮小**: `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体験ページ）](/demo/no-js-modern-showcase/)**

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

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

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

https://zenn.dev/forcia_tech/articles/202606_tskaigi_othello_hatano

https://github.com/codemix/ts-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` を駆使して **「数値計算・値の伝搬」** を行う

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

https://zenn.dev/jigjp_engineer/articles/c1cc55635c6039

  - まさにTS型オセロのCSS版。ラジオボタンによるCheckbox Hackすら使わず、`div:hover:active`、`sibling-index()`、カウンター機能、さらには `animation-timeline: view()` まで組み合わせて盤面の挟み込み判定をCSSだけで記述しています。

https://zenn.dev/kamekyame/articles/1da0e24830e2b8

  - 8つの `<details>` の開閉状態（0/1）と `sibling-index()`、CSSカウンターを連動させ、二進数の入力値を純CSSでリアルタイム計算・表示している解説記事。

https://css-tricks.com/css-elevator-a-pure-css-state-machine-with-floor-navigation/

  - ラジオボタンやCSS変数、カウンターを駆使して、エレベーターの階層移動と状態管理を純CSSで実装した詳細な解説記事。


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

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

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

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

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

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

## 参考リンク・リソース

- [MDN Web Docs: Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
- [MDN Web Docs: CSS Anchor Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning)
- [Chrome for Developers: Introducing the Popover API](https://developer.chrome.com/blog/introducing-popover-api/)
- [Chrome for Developers: The CSS Anchor Positioning API](https://developer.chrome.com/blog/anchor-positioning-api/)
- [MDN Web Docs: CSS scroll-driven animations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll-driven_animations)
- [Scroll-driven Animations Demos & Tools](https://scroll-driven-animations.style/)
- [Chrome for Developers: Inside look at modern web browser (part 3)](https://developer.chrome.com/blog/inside-browser-part3/)
- [web.dev: Stick to Compositor-Only Properties and Manage Layer Count](https://web.dev/articles/stick-to-compositor-only-properties-and-manage-layer-count)
- [Zenn: TypeScriptの型のみでオセロの型を実装したので解説する](https://zenn.dev/forcia_tech/articles/202606_tskaigi_othello_hatano)
- [Zenn: JavaScriptゼロ！CSS（とHTML）だけで作るリバーシゲーム](https://zenn.dev/jigjp_engineer/articles/c1cc55635c6039)
- [Zenn: CSSで作る二進計算機](https://zenn.dev/kamekyame/articles/1da0e24830e2b8)
- [CSS-Tricks: CSS Elevator: A Pure CSS State Machine](https://css-tricks.com/css-elevator-a-pure-css-state-machine-with-floor-navigation/)