はじめに
「このタブ切り替え、アコーディオン、モーダル……全部JavaScriptで実装しなきゃ」と、条件反射的にstateやイベントリスナーを書き始めていませんか。実はその多くが、CSSだけで十分に実現できます。
You-Dont-Need-JavaScript は、まさにその発想を体現したリソース集です。:checked や :target、隣接セレクタといったCSSの機能を駆使することで、JavaScriptなしでもインタラクティブなUIが作れることを、実例とともに示してくれます。バンドルサイズを減らしたい、シンプルな実装で済ませたいと考えているフロントエンドエンジニアにとって、発想の引き出しを増やしてくれる一冊ならぬ「一リポジトリ」です。
とはいえ、読むより触った方が早いと思います。CSSの:targetだけで開閉するモーダルウィンドウを実際に動かせる形で用意したので、先に挙動を見たい方はこちらからどうぞ。
You-Dont-Need-JavaScriptとは
You-Dont-Need-JavaScript は、GitHub上で公開されているオープンソースのリソース集で、「CSS is powerful, you can do a lot of things without JS.」というコンセプトのもと、CSSのみで実装したUIコンポーネントのサンプル集をまとめています。npmパッケージとして配布されているライブラリではなく、コードスニペットとデモを集めたリファレンスという位置づけです。
主な特徴
- 豊富なコンポーネント例 - アコーディオン、タブ、ドロップダウンメニュー、モーダル、カルーセル、ツールチップなど、実務でよく使うUIパターンが網羅されています
- コピペで使えるコード - 各サンプルは単体のHTML/CSSとして完結しており、そのままプロジェクトに組み込めます
- JS依存を減らす発想の学習 -
:checked、:target、:hover、隣接セレクタといったCSSセレクタの応用力を学べる教材としても優秀です
なお、READMEにも明記されている通り、ここで紹介されているテクニックはあくまで「CSSでもここまでできる」という概念実証(Proof of Concept)です。キーボード操作やスクリーンリーダーへの対応など、アクセシビリティ面で課題が残るサンプルも含まれているため、実プロダクトに導入する際はその点を補強する必要があります。
インストール
You-Dont-Need-JavaScript はnpmパッケージではなく、GitHub上のリポジトリとして公開されているコードスニペット集です。インストールというよりは、必要な部分を確認してコードを取り込む形で利用します。
# リポジトリをクローンしてローカルで確認する場合
git clone https://github.com/you-dont-need/You-Dont-Need-JavaScript.git
各サンプルはHTMLとCSSのみで完結しているため、READMEやデモページから該当箇所をコピーし、自分のプロジェクトのマークアップとスタイルに合わせて調整するだけで使い始められます。
You-Dont-Need-JavaScriptのサンプルを動かす
You-Dont-Need-JavaScript の中でも特に「へえ」となりやすいのが、CSSの:target擬似クラスだけで実装するモーダルウィンドウです。JavaScriptのクリックイベントを1行も書かず、<a href="#demo-modal">のようなアンカーリンクとURLフラグメント(#以降の文字列)の一致を:targetで検知して、表示・非表示を切り替えます。下のサンプルは実際に編集・実行できるので、まずは「モーダルを開く」リンクを押して挙動を確かめてみてください。
:targetは、現在のURLのフラグメント識別子が対象要素のidと一致しているときにだけマッチする擬似クラスです。要点だけを抜き出すと、次のようになります。
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
align-items: center;
justify-content: center;
}
/* URLの#がmodal-overlayのidと一致した瞬間だけ表示 */
.modal-overlay:target {
display: flex;
}
.modal-close {
position: absolute;
inset: 0; /* オーバーレイ全体をクリックで閉じられるようにする */
}
実際に動かせるものが下です。「モーダルを開く」を押すとURLの末尾に#demo-modalが付与されて:targetが発火し、オーバーレイが表示されます。「閉じる」ボタンやオーバーレイの余白部分を押して#だけのURLに戻ると、:targetが外れて非表示に戻ります。
このサンプルのid="demo-modal"を別の名前に変え、対応するhrefも揃えれば、同じページ内に複数の独立したモーダルを共存させられます。ページ遷移やブラウザバックとも自然に連動する(#がブラウザの履歴に積まれる)のも、:targetを使ったモーダルならではの特徴です。
基本的な使い方
代表的な例として、JavaScriptなしでタブ切り替えを実装してみましょう。ポイントは、非表示のradio要素とlabelを組み合わせ、:checked状態に応じて表示を切り替えるところです。
<div class="tabs">
<input type="radio" name="tab-group" id="tab1" checked class="tab-input" />
<label for="tab1" class="tab-label">概要</label>
<input type="radio" name="tab-group" id="tab2" class="tab-input" />
<label for="tab2" class="tab-label">使い方</label>
<input type="radio" name="tab-group" id="tab3" class="tab-input" />
<label for="tab3" class="tab-label">FAQ</label>
<div class="tab-content" id="content1">CSSだけでタブを実装できます。</div>
<div class="tab-content" id="content2">radioとlabelを組み合わせるのがコツです。</div>
<div class="tab-content" id="content3">JavaScriptは一切使っていません。</div>
</div>
.tabs {
display: flex;
flex-wrap: wrap;
}
.tab-input {
display: none;
}
.tab-label {
padding: 0.5rem 1rem;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab-content {
display: none;
width: 100%;
padding: 1rem 0;
}
/* チェックされたタブに対応するコンテンツだけを表示 */
#tab1:checked ~ #content1,
#tab2:checked ~ #content2,
#tab3:checked ~ #content3 {
display: block;
}
.tab-input:checked + .tab-label {
border-bottom-color: currentColor;
font-weight: bold;
}
radioのname属性を揃えることで排他選択になり、:checkedの状態が変わるたびに、隣接する要素の表示・非表示がCSSだけで切り替わります。クリックイベントを拾ってstateを更新する処理は一切不要です。
実践的なユースケース
checkboxと:checkedで作るアコーディオン
続いて、実務でよく使うアコーディオンをCSSのみで実装する例です。checkboxを使えば、複数の項目を独立して開閉できます。
<div class="accordion">
<input type="checkbox" id="acc1" class="accordion-input" />
<label for="acc1" class="accordion-header">利用規約について</label>
<div class="accordion-body">
ここに詳細な説明文が入ります。checkboxの状態に応じて開閉します。
</div>
<input type="checkbox" id="acc2" class="accordion-input" />
<label for="acc2" class="accordion-header">プライバシーポリシーについて</label>
<div class="accordion-body">
こちらも同様に、CSSだけで開閉アニメーションを実現しています。
</div>
</div>
.accordion-input {
display: none;
}
.accordion-header {
display: block;
padding: 0.75rem 1rem;
background: #f5f5f5;
cursor: pointer;
}
.accordion-body {
max-height: 0;
overflow: hidden;
padding: 0 1rem;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.accordion-input:checked + .accordion-header + .accordion-body {
max-height: 200px;
padding: 0.75rem 1rem;
}
max-heightをtransitionさせることで、開閉時になめらかなアニメーションもつきます。ちょっとしたFAQページやサイドバーのメニュー開閉であれば、これだけでReactやVueのコンポーネントを1つ書かずに済んでしまいます。バンドルサイズを気にする軽量なランディングページや、静的サイトジェネレーターで生成したページに組み込む際に特に相性が良いテクニックです。
実際に動かせるものが下です。それぞれの項目をクリックして開閉し、複数のパネルが同時に開いたままになること(checkboxはそれぞれ独立しているため排他制御にはならない)も確認してみてください。
.accordion-input:checked + .accordion-header + .accordion-bodyという隣接セレクタの連鎖が肝です。checkboxのidとlabelのforをセットで増やすだけで、項目数はいくらでも追加できます。
:hoverと:focus-withinで作るドロップダウンメニュー
ナビゲーションのドロップダウンメニューも、You-Dont-Need-JavaScriptが扱う定番パターンの1つです。マウス操作は:hoverだけで実現できますが、それだけだとキーボード操作のユーザーがサブメニューを開けません。そこで:focus-withinを組み合わせ、メニュー内の要素にフォーカスが当たっている間もサブメニューを開いたままにします。
.dropdown:hover .submenu,
.dropdown:focus-within .submenu {
display: block;
}
.submenu {
display: none;
position: absolute;
}
実際に動かせるものが下です。マウスでメニュー項目にカーソルを合わせるとサブメニューが開きます。マウスを使わず、Tabキーでメニュー内のリンクにフォーカスを移動させても同じように開くことを確認してみてください。
:focus-withinは「自分自身、または子孫要素のいずれかがフォーカスされている」ときにマッチする擬似クラスです。.dropdown要素に付けておくことで、サブメニュー内のリンクにフォーカスが移っている間は.dropdown:hoverが外れてもメニューが閉じない、という状態を作れます。
:checkedとラジオボタンで作るタブ切り替え
「基本的な使い方」で紹介したタブ切り替えを、実際に手を動かして試せる形でも用意しました。radioのname属性を揃えて排他選択にし、:checkedになった要素に対応するコンテンツだけを表示する仕組みです。
先ほどのアコーディオンとの違いに注目してください。アコーディオンはcheckbox(複数選択可)を使っていたのに対し、こちらはnameを共有したradio(単一選択)を使うことで、タブが同時に1つしか開かない排他的な切り替えを実現しています。タブの数を増やしたいときは、radio・label・.tab-contentの組を追加し、name属性と#id:checked ~ #contentの対応関係を揃えるだけです。
まとめ
You-Dont-Need-JavaScript は、タブやアコーディオン、モーダルといった「JavaScriptが必須」だと思い込みがちなUIの多くが、実はCSSの:checkedや隣接セレクタだけで実装できることを教えてくれるリソース集でした。すべてのケースでJSを置き換えるべきというわけではありませんが、「本当にJSが必要か」を一度立ち止まって考える良いきっかけになります。
まずはリポジトリのデモをいくつか眺めて、自分のプロジェクトで使えそうなパターンを探してみてください。CSSセレクタへの理解が深まると同時に、より軽量でシンプルな実装の引き出しが増えるはずです。