作成日:2025/8/13,最終更新日:2025/8/13

JavaScript / TypeScriptのDOMとイベントを組み合わせた実装について

DOM操作とイベント処理を組み合わせて、よくあるUIを最小実装で作る方法に関する解説

この記事では、DOMの追加・削除やクラス切り替えと、イベント伝播・デリゲーションなどの考え方を組み合わせて、 代表的なUIパターンをコンパクトに実装した例を紹介します。


1. タブ切り替えUI

解説: クリック(とキーボード操作)で表示領域を切り替える、定番UIの実装です。
イベントデリゲーションでコード量を最小化しつつ、アクセシビリティ(role/aria)にも配慮します。




● 基本構造(HTML)

タブはボタン、パネルはセクション要素として分離し、タブとパネルを data属性と id で結び付けます。

<!-- タブ群 -->
<div class="tabs" id="tabsDemo">
  <div class="tab-list" role="tablist" aria-label="サンプルのタブ">
    <button class="tab is-active" role="tab" aria-selected="true" tabindex="0"
            data-tab="a" aria-controls="panel-a">タブA</button>
    <button class="tab" role="tab" aria-selected="false" tabindex="-1"
            data-tab="b" aria-controls="panel-b">タブB</button>
    <button class="tab" role="tab" aria-selected="false" tabindex="-1"
            data-tab="c" aria-controls="panel-c">タブC</button>
  </div>

  <!-- パネル群 -->
  <div class="tab-panels">
    <section class="panel is-active" role="tabpanel" id="panel-a" aria-labelledby="tab-a">
      内容A:ここに任意のコンテンツ
    </section>
    <section class="panel" role="tabpanel" id="panel-b" aria-labelledby="tab-b" hidden>
      内容B:ここに任意のコンテンツ
    </section>
    <section class="panel" role="tabpanel" id="panel-c" aria-labelledby="tab-c" hidden>
      内容C:ここに任意のコンテンツ
    </section>
  </div>
</div>

表示切替は、.is-active クラスと hidden 属性を使い分けます(CSSは後述の最小例を参照)。


● 振る舞い(JavaScript)

クリックと矢印キーでタブを切り替えます。イベントは親要素ひとつにまとめるのがポイントです。

// 親にまとめてイベントを付与(デリゲーション)
const root = document.getElementById('tabsDemo');
const tabSelector = '.tab-list .tab';
const panelSelector = '.tab-panels .panel';

function activateTab(btn) {
  if (!btn) return;
  const name = btn.dataset.tab;
  const list = root.querySelector('.tab-list');
  const tabs = root.querySelectorAll(tabSelector);
  const panels = root.querySelectorAll(panelSelector);

  // タブの見た目とフォーカス管理
  tabs.forEach(t => {
    const isActive = t === btn;
    t.classList.toggle('is-active', isActive);
    t.setAttribute('aria-selected', String(isActive));
    t.setAttribute('tabindex', isActive ? '0' : '-1');
  });

  // パネルの表示切替
  panels.forEach(p => {
    const show = p.id === btn.getAttribute('aria-controls');
    p.toggleAttribute('hidden', !show);
    p.classList.toggle('is-active', show);
  });

  // キーボード操作時のフォーカス移動
  btn.focus();
}

// クリックで切替
root.addEventListener('click', (e) => {
  const btn = e.target.closest('.tab');
  if (!btn || !root.contains(btn)) return;
  activateTab(btn);
});

// キーボード操作(左右キーで移動、Home/End対応)
root.addEventListener('keydown', (e) => {
  const current = root.querySelector(`${tabSelector}[aria-selected="true"]`);
  if (!current) return;

  const tabs = [...root.querySelectorAll(tabSelector)];
  const idx = tabs.indexOf(current);

  let nextIdx = idx;
  if (e.key === 'ArrowRight') nextIdx = (idx + 1) % tabs.length;
  if (e.key === 'ArrowLeft')  nextIdx = (idx - 1 + tabs.length) % tabs.length;
  if (e.key === 'Home')       nextIdx = 0;
  if (e.key === 'End')        nextIdx = tabs.length - 1;

  if (nextIdx !== idx) {
    e.preventDefault();
    activateTab(tabs[nextIdx]);
  }
});

● 最小CSS(見た目の切り替え)

必須ではありませんが、表示のオン・オフに関わる最小CSSを載せておきます。既存のCSSと競合しないよう、必要に応じて調整してください。

/* タブの見た目(必要に応じて調整) */
.tab-list { display: flex; gap: .5rem; }
.tab { padding: .4rem .8rem; border: 1px solid #ccc; background: #f7f7f7; }
.tab.is-active { background: #e6f2ff; border-color: #87bfff; }

/* パネル切り替え */
.panel[hidden] { display: none !important; }
.panel.is-active { animation: fadeIn .15s ease; }

@keyframes fadeIn {
  from { opacity: 0 } to { opacity: 1 }
}

● 実行例(デモ)

下のデモで、タブをクリックまたは矢印キーで切り替えて挙動を確認してください。

デモ内容1

● まとめ

  • クリックとキー操作を親要素ひとつで受けると、コードがシンプルで拡張しやすい
  • タブは role/aria/hidden の組み合わせで、見た目とアクセシビリティの両立が可能
  • 表示切替の実体は、クラス(is-active)と hidden の切り替えに集約すると分かりやすい

2. アコーディオンメニュー

解説: アコーディオンメニューは、クリックでパネルを開閉するUIです。
ナビゲーションやFAQ、項目の詳細表示など、限られたスペースに情報を整理するのに適しています。
DOM操作では「クラス切り替え」や「hidden属性の付け外し」、イベント処理では「クリックイベントの伝播制御」がよく使われます。




● 基本構文と使い方

見出し部分をクリックすると、直下のコンテンツが開閉されるようにします。
CSSで開閉時のスタイルやアニメーションを定義し、JavaScriptでクラスや属性を操作します。

// 単純な開閉処理例
document.querySelectorAll('.accordion-header').forEach(header => {
  header.addEventListener('click', () => {
    const panel = header.nextElementSibling;
    panel.hidden = !panel.hidden;
    header.classList.toggle('is-open', !panel.hidden);
  });
});

● 実行例(デモ)

下のデモで見出しをクリックすると、該当のパネルだけが開閉します。
1つだけ開く「アコーディオン型」になるように設定しています。


● 注意点とベストプラクティス

  • 開閉アニメーションを付ける場合はCSSのmax-heightやtransformを使うと滑らかになる
  • FAQや長文では、スクロール位置の制御も加えるとUXが向上する
  • アクセシビリティ対応には、aria-expanded属性やaria-controls属性を付与する

● まとめ

  • アコーディオンメニューは情報をコンパクトに整理するのに有効
  • 「1つだけ開く」「複数同時に開く」どちらも簡単に実装可能
  • CSSとJSを組み合わせて、見た目と挙動をシンプルに保つのがポイント

3. スクロールでフェードイン

解説: ページをスクロールしたときに要素が表示範囲に入ったら、ふわっと表示させる演出です。
IntersectionObserver を使うと、スクロール位置をイベントで逐一監視するのではなく、 ブラウザが効率よく検知してくれるため、パフォーマンスに優れた実装が可能です。




● 基本構文と使い方

IntersectionObserver は、指定要素がビューポート内に入った/出たタイミングでコールバックを呼びます。
ここでは、要素が 20% 以上見えたらクラスを付与してフェードインさせる例を示します。

const targets = document.querySelectorAll('.fadein-target');

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target); // 一度表示したら監視解除
    }
  });
}, { threshold: 0.2 });

targets.forEach(el => observer.observe(el));

● 実行例(デモ)

下に並んだボックスは、スクロールして画面内に入るとフェードインします。
※画面を縮小して縦スクロールを発生させると動きが確認できます。

スクロールしてみてください ↓
Box 1
Box 2
Box 3

● 応用のヒント

  • 一度だけでなく、入るたびにアニメーションしたい場合は監視解除をしない
  • threshold を上げると、より多く見えたタイミングで発火
  • root や rootMargin を設定して、特定のスクロールコンテナ内だけで監視可能

● まとめ

  • IntersectionObserver を使うと、スクロール位置を効率よく監視できる
  • パフォーマンスが良く、ネイティブで使えるアニメーションのトリガーとして最適
  • CSSトランジションやアニメーションと組み合わせることで表現力が広がる

4. リストの動的追加・削除

解説: ボタン操作でリスト項目を追加・削除できるUIです。
DOM操作(要素の生成・削除)とイベントデリゲーション(親要素にまとめてイベントを付与)を組み合わせることで、シンプルかつ拡張性のある実装が可能です。




● 基本構文と使い方

追加時は createElement で li 要素を作り、appendChild でリスト末尾に追加します。
削除は各 li の「削除ボタン」をクリックしたときに remove() を呼びます。
イベントデリゲーションを使うことで、追加された要素にも自動的にイベントが適用されます。

const list = document.getElementById('myList');
const addBtn = document.getElementById('addBtn');

addBtn.addEventListener('click', () => {
  const li = document.createElement('li');
  li.innerHTML = `新しい項目 `;
  list.appendChild(li);
});

list.addEventListener('click', (e) => {
  if (e.target.classList.contains('delete-btn')) {
    e.target.closest('li').remove();
  }
});

● 実行例(デモ)

下のデモで「項目を追加」ボタンをクリックすると、新しい項目が追加されます。
各項目の「削除」ボタンを押すと、その項目が消えます。

  • 項目 1
  • 項目 2

● 応用のヒント

  • 削除時に確認ダイアログを表示して誤操作防止
  • 項目内容を input 要素にして、その場で編集できるようにする
  • localStorage や IndexedDB と組み合わせて、リロード後も状態保持

● まとめ

  • イベントデリゲーションを使うと、追加要素にもイベントが自動で適用される
  • DOM操作は createElement / appendChild / remove を覚えておくと便利
  • 動的UIはシンプルなパターンでも実務で多用される