HOME / BLOG
BLOG

JavaScriptで作るよく見るUI 10選【コピペできる実装例】

デフォルト画像

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のdetailssummaryでも実装できます。独自の状態連携が必要なときに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記事

参考資料

まとめ

よく見るUIは、クリックイベントだけなら短いコードで作れます。しかし実務で大切なのは、開閉状態、キーボード操作、フォーカス、閉じ方まで一つの部品として設計することです。まずはアコーディオンやコピーボタンのような小さなUIから始め、同じ考え方をタブやモーダルへ広げてみてください。