Webサイトや管理画面では、開閉するアコーディオン、表示を切り替えるタブ、画面の手前に出るモーダルなど、何度も目にするUIがあります。仕組みを一度理解すると、ライブラリへ頼らず小さな機能を組み立てられるようになります。
この記事では、JavaScriptで作るよく見るUIを10種類に絞り、コピペして試せる最小コードを紹介します。見た目より先に、キーボード操作、状態属性、閉じ方まで含めて実装するのがポイントです。
実装前に共通で押さえたいこと
- クリックできる要素には、原則として
divではなくbuttonを使う - 開閉状態は
aria-expanded、選択状態はaria-selectedなど、見た目以外の情報でも伝える Escapeキー、外側クリック、フォーカス復帰など「閉じる操作」を用意するhidden属性を使う場合は、表示とアクセシビリティツリーを同時に切り替える- 同じUIを複数置く可能性があるなら、IDや単一要素へ依存しすぎない
[hidden] {
display: none !important;
}
button {
font: inherit;
}
:focus-visible {
outline: 3px solid #00a3ff;
outline-offset: 3px;
}1. アコーディオン
質問と回答、補足情報、絞り込み条件などを必要なときだけ開くUIです。ボタンと開閉パネルを関連付けます。
HTML
<section class="accordion">
<button
class="accordion-button"
type="button"
aria-expanded="false"
aria-controls="answer-1"
>
送料はいくらですか?
</button>
<div id="answer-1" class="accordion-panel" hidden>
<p>5,000円以上の購入で送料無料です。</p>
</div>
</section>JavaScript
document.querySelectorAll(".accordion-button").forEach((button) => {
button.addEventListener("click", () => {
const panel = document.querySelector(`#${button.getAttribute("aria-controls")}`);
const willOpen = button.getAttribute("aria-expanded") === "false";
button.setAttribute("aria-expanded", String(willOpen));
panel.hidden = !willOpen;
});
});実装メモ:FAQだけならHTMLのdetailsとsummaryでも実装できます。独自の状態連携が必要なときにJavaScript版を選びます。
2. タブ切り替え
同じ領域で内容を切り替えるUIです。選択中のタブだけをTabキーの移動対象にし、左右キーでも移動できるようにします。
HTML
<div class="tabs">
<div role="tablist" aria-label="商品情報">
<button id="tab-feature" role="tab" aria-selected="true" aria-controls="panel-feature">特徴</button>
<button id="tab-spec" role="tab" aria-selected="false" aria-controls="panel-spec" tabindex="-1">仕様</button>
</div>
<section id="panel-feature" role="tabpanel" aria-labelledby="tab-feature">軽くて持ち運びやすい製品です。</section>
<section id="panel-spec" role="tabpanel" aria-labelledby="tab-spec" hidden>重量は680gです。</section>
</div>JavaScript
const tabs = [...document.querySelectorAll('[role="tab"]')];
function selectTab(nextTab) {
tabs.forEach((tab) => {
const selected = tab === nextTab;
tab.setAttribute("aria-selected", String(selected));
tab.tabIndex = selected ? 0 : -1;
document.querySelector(`#${tab.getAttribute("aria-controls")}`).hidden = !selected;
});
nextTab.focus();
}
tabs.forEach((tab, index) => {
tab.addEventListener("click", () => selectTab(tab));
tab.addEventListener("keydown", (event) => {
if (!["ArrowLeft", "ArrowRight"].includes(event.key)) return;
event.preventDefault();
const step = event.key === "ArrowRight" ? 1 : -1;
selectTab(tabs[(index + step + tabs.length) % tabs.length]);
});
});実装メモ:スマホ幅では、タブ名が長いと横スクロールが必要になることがあります。2〜4項目程度に絞ると扱いやすくなります。
3. モーダルダイアログ
確認、入力、詳細表示を画面の手前で行うUIです。ネイティブのdialog要素を使うと、モーダル表示とフォーカス管理の土台を作りやすくなります。
HTML
<button id="open-dialog" type="button">削除を確認する</button>
<dialog id="confirm-dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">本当に削除しますか?</h2>
<p>この操作は元に戻せません。</p>
<form method="dialog">
<button value="cancel">キャンセル</button>
<button value="confirm">削除する</button>
</form>
</dialog>JavaScript
const openButton = document.querySelector("#open-dialog");
const dialog = document.querySelector("#confirm-dialog");
openButton.addEventListener("click", () => dialog.showModal());
dialog.addEventListener("click", (event) => {
if (event.target === dialog) dialog.close("cancel");
});
dialog.addEventListener("close", () => {
if (dialog.returnValue === "confirm") {
console.log("削除処理を実行");
}
});実装メモ:重要な処理では、閉じるボタン、Escapeキー、処理後の結果表示まで確認します。ARIA属性だけではモーダルの動作は完成しません。
4. ドロップダウンメニュー
プロフィールメニューや補助操作をまとめるUIです。ボタンの外側を押したときとEscapeキーでも閉じます。
HTML
<div class="dropdown">
<button class="dropdown-button" type="button" aria-expanded="false" aria-controls="user-menu">アカウント</button>
<ul id="user-menu" hidden>
<li><a href="/profile/">プロフィール</a></li>
<li><button type="button">ログアウト</button></li>
</ul>
</div>JavaScript
const dropdown = document.querySelector(".dropdown");
const dropdownButton = dropdown.querySelector(".dropdown-button");
const menu = dropdown.querySelector("#user-menu");
function closeMenu() {
dropdownButton.setAttribute("aria-expanded", "false");
menu.hidden = true;
}
dropdownButton.addEventListener("click", () => {
const willOpen = menu.hidden;
menu.hidden = !willOpen;
dropdownButton.setAttribute("aria-expanded", String(willOpen));
});
document.addEventListener("click", (event) => {
if (!dropdown.contains(event.target)) closeMenu();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeMenu();
});実装メモ:選択肢を選ぶフォームなら、独自メニューよりネイティブのselectを優先します。用途に合う要素を選ぶことが重要です。
5. ドロワーメニュー
スマホのナビゲーションなどを画面端から開くUIです。開いている間はボタンの状態とパネルの表示を同期します。
HTML
<button class="drawer-button" type="button" aria-expanded="false" aria-controls="site-drawer">メニュー</button>
<aside id="site-drawer" class="drawer" hidden>
<nav aria-label="メインメニュー">
<a href="/">ホーム</a>
<a href="/blog/">ブログ</a>
</nav>
<button class="drawer-close" type="button">閉じる</button>
</aside>JavaScript
const drawerButton = document.querySelector(".drawer-button");
const drawer = document.querySelector("#site-drawer");
const drawerClose = drawer.querySelector(".drawer-close");
function setDrawer(open) {
drawer.hidden = !open;
drawerButton.setAttribute("aria-expanded", String(open));
document.body.classList.toggle("is-drawer-open", open);
(open ? drawerClose : drawerButton).focus();
}
drawerButton.addEventListener("click", () => setDrawer(true));
drawerClose.addEventListener("click", () => setDrawer(false));
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !drawer.hidden) setDrawer(false);
});実装メモ:本格的なモーダル型ドロワーでは、背面の操作停止とフォーカス移動範囲も設計します。ナビが少ないなら常時表示も検討します。
6. トースト通知
保存完了やコピー完了など、短い結果を一時表示するUIです。スクリーンリーダーにも伝わるようrole="status"を使います。
HTML
<button id="save-button" type="button">保存する</button>
<p id="toast" class="toast" role="status" aria-live="polite" hidden></p>JavaScript
const saveButton = document.querySelector("#save-button");
const toast = document.querySelector("#toast");
let toastTimer;
function showToast(message) {
clearTimeout(toastTimer);
toast.textContent = message;
toast.hidden = false;
toastTimer = setTimeout(() => {
toast.hidden = true;
}, 2500);
}
saveButton.addEventListener("click", () => showToast("保存しました"));実装メモ:エラーや確認が必要な情報は自動で消さず、本文内のメッセージやダイアログを使います。トーストは補助的な完了通知向けです。
7. ツールチップ
アイコンの意味など、短い補足を表示するUIです。マウスだけでなく、キーボードフォーカスでも表示します。
HTML
<span class="tooltip-wrap">
<button class="help-button" type="button" aria-describedby="help-tooltip">?</button>
<span id="help-tooltip" role="tooltip" hidden>公開後はURLを変更できません</span>
</span>JavaScript
const helpButton = document.querySelector(".help-button");
const tooltip = document.querySelector("#help-tooltip");
const showTooltip = () => { tooltip.hidden = false; };
const hideTooltip = () => { tooltip.hidden = true; };
helpButton.addEventListener("mouseenter", showTooltip);
helpButton.addEventListener("mouseleave", hideTooltip);
helpButton.addEventListener("focus", showTooltip);
helpButton.addEventListener("blur", hideTooltip);
helpButton.addEventListener("keydown", (event) => {
if (event.key === "Escape") hideTooltip();
});実装メモ:重要な説明をツールチップだけに隠さないでください。スマホではホバーがないため、タップや常時表示も検討します。
8. パスワード表示切り替え
入力したパスワードを確認できるようにするUIです。入力値は変えず、inputのtypeとボタンのラベルを切り替えます。
HTML
<label for="password">パスワード</label>
<div class="password-field">
<input id="password" type="password" autocomplete="current-password">
<button class="password-toggle" type="button" aria-pressed="false">表示する</button>
</div>JavaScript
const password = document.querySelector("#password");
const passwordToggle = document.querySelector(".password-toggle");
passwordToggle.addEventListener("click", () => {
const willShow = password.type === "password";
password.type = willShow ? "text" : "password";
passwordToggle.setAttribute("aria-pressed", String(willShow));
passwordToggle.textContent = willShow ? "隠す" : "表示する";
});実装メモ:ボタンを押したときにinputを作り直す必要はありません。入力位置やオートコンプリートを保ったままtypeだけを変更します。
9. 文字数カウンター
プロフィール文や投稿フォームで、入力済み文字数と上限を表示するUIです。入力のたびにtextContentを更新します。
HTML
<label for="bio">プロフィール</label>
<textarea id="bio" maxlength="120" aria-describedby="bio-count"></textarea>
<p id="bio-count"><output>0</output> / 120文字</p>JavaScript
const bio = document.querySelector("#bio");
const countOutput = document.querySelector("#bio-count output");
function updateCount() {
countOutput.textContent = bio.value.length;
}
bio.addEventListener("input", updateCount);
updateCount();実装メモ:見た目のカウンターだけで制限せず、HTMLのmaxlengthやサーバー側の検証も組み合わせます。
10. コピーボタン
URL、招待コード、コードスニペットなどをワンクリックでコピーするUIです。成功と失敗の両方を利用者へ返します。
HTML
<div class="copy-area">
<code id="copy-target">npm run build</code>
<button class="copy-button" type="button" data-copy-target="copy-target">コピー</button>
<span class="copy-status" role="status"></span>
</div>JavaScript
const copyButton = document.querySelector(".copy-button");
const copyStatus = document.querySelector(".copy-status");
copyButton.addEventListener("click", async () => {
const target = document.querySelector(`#${copyButton.dataset.copyTarget}`);
try {
await navigator.clipboard.writeText(target.textContent);
copyStatus.textContent = "コピーしました";
} catch (error) {
copyStatus.textContent = "コピーできませんでした";
}
});実装メモ:Clipboard APIは安全なコンテキストで利用します。長いコードでは、手動選択できる表示も残しておくと安心です。
よく見るUIを作るときのチェックリスト
- マウスを使わずTabキーとEnter・Spaceキーで操作できるか
- 開閉・選択状態を属性でも伝えているか
- Escapeキーや閉じるボタンで元の画面へ戻れるか
- 連打や通信失敗が起きても表示が破綻しないか
- JavaScriptが動かない場合も最低限の情報へ到達できるか
- スマホで押しやすいサイズと余白になっているか
関連するJavaScript記事
- JavaScriptで要素が画面に入ったらアニメーションする方法
- チェックボックスでdisabledを切り替える方法
- JavaScriptで画面サイズの変化を検知する方法
- WordPressオリジナルテーマにダークモードを実装する方法
参考資料
まとめ
よく見るUIは、クリックイベントだけなら短いコードで作れます。しかし実務で大切なのは、開閉状態、キーボード操作、フォーカス、閉じ方まで一つの部品として設計することです。まずはアコーディオンやコピーボタンのような小さなUIから始め、同じ考え方をタブやモーダルへ広げてみてください。