🛠️

hidden属性+data属性だけでUIを制御する汎用JavaScript

に公開

🎯 背景

Web制作をしていると「Aを押したらBが出る/消える」といったシンプルなUI制御はとてもよく出てきます。
毎回個別にJSを書くのは面倒なので、data属性だけで汎用的に扱えるJS を備忘録としてまとめました。


🚀 仕様

  • 初期状態の管理はHTML+CSS(非表示は hidden 属性)
  • 挙動制御はすべて data- 属性で指定
  • JSはクリックイベントを拾うだけの最小構成
  • 要素が存在しなくてもエラーにならない安全設計
  • アクセシビリティ対応(aria-hiddeninert

🛠 データ属性の設計

  • data-ui="act" … トリガー要素に付与
  • data-behavior … 挙動タイプ
    • hide → クリックで対象を消す
    • show → クリックで対象を表示する
    • toggle → 押すたびに表示/非表示を切り替える
    • show-hide-self → 対象を出しつつ自分は消える
  • data-target … 対象要素のセレクタ(複数指定可)

💻 実装コード

JS本体

<script>
(() => {
  const HIDDEN_CLASS = 'js-hidden';

  const hideEl = (el) => {
    if (!el) return;
    el.classList.add(HIDDEN_CLASS);
    el.setAttribute('aria-hidden', 'true');
    el.setAttribute('inert', '');
  };

  const showEl = (el) => {
    if (!el) return;
    el.classList.remove(HIDDEN_CLASS);
    el.removeAttribute('aria-hidden');
    el.removeAttribute('inert');
  };

  const toggleEl = (el) => {
    if (!el) return;
    el.classList.contains(HIDDEN_CLASS) ? showEl(el) : hideEl(el);
  };

  const selectTargets = (selector) => {
    if (!selector) return [];
    try {
      return selector
        .split(',')
        .map(s => s.trim())
        .filter(Boolean)
        .flatMap(s => Array.from(document.querySelectorAll(s)));
    } catch {
      return [];
    }
  };

  const convertHiddenAttr = (root = document) => {
    const nodes = root.querySelectorAll('[hidden]');
    nodes.forEach(el => {
      el.classList.add(HIDDEN_CLASS);
      el.removeAttribute('hidden');
      el.setAttribute('aria-hidden', 'true');
      el.setAttribute('inert', '');
    });
  };

  document.addEventListener('click', (e) => {
    const trigger = e.target.closest('[data-ui="act"]');
    if (!trigger) return;

    const behavior = trigger.getAttribute('data-behavior') || 'toggle';
    const targets = selectTargets(trigger.getAttribute('data-target'));

    if (targets.length === 0 && behavior !== 'show-hide-self') return;

    switch (behavior) {
      case 'hide': targets.forEach(hideEl); break;
      case 'show': targets.forEach(showEl); break;
      case 'toggle': targets.forEach(toggleEl); break;
      case 'show-hide-self': targets.forEach(showEl); hideEl(trigger); break;
    }
  });

  document.addEventListener('DOMContentLoaded', () => {
    convertHiddenAttr();

    const mo = new MutationObserver((records) => {
      for (const r of records) {
        r.addedNodes.forEach(node => {
          if (!(node instanceof Element)) return;
          if (node.hasAttribute?.('hidden')) {
            convertHiddenAttr(node.ownerDocument || document);
          }
          const childHidden = node.querySelectorAll?.('[hidden]');
          if (childHidden && childHidden.length) {
            convertHiddenAttr(node);
          }
        });
        if (r.type === 'attributes' && r.attributeName === 'hidden' && r.target instanceof Element) {
          convertHiddenAttr(r.target.parentNode || document);
        }
      }
    });
    mo.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['hidden']
    });
  });
})();
</script>

cssは1行のみ

.js-hidden { display: none !important; }

📦 HTMLサンプル

1) クリックで消す

<button type="button" data-ui="act" data-behavior="hide" data-target="#box1">
  Box1を隠す
</button>
<div id="box1">Box1</div>

2) クリックで表示

<button type="button" data-ui="act" data-behavior="show" data-target="#box2">
  Box2を表示
</button>
<div id="box2" hidden>Box2(初期はhidden)</div>

3) トグル(表示/非表示)

<button type="button" data-ui="act" data-behavior="toggle" data-target="#box3">
  Box3をトグル
</button>
<div id="box3">Box3</div>

4) 表示+自分を消す

<button type="button" data-ui="act" data-behavior="show-hide-self" data-target="#box4">
  Box4を表示(自分は消える)
</button>
<div id="box4" hidden>Box4</div>

🔧 <a><input><div> で使うときの応用

今回の仕組みは「data-ui="act" を持つ要素」なら何でもトリガーにできます。
ただし要素の種類によって気をつける点があります。

1. <a> タグの場合

href を付けるとクリック時にページ遷移してしまうので、JSで止めるか href="#" にするのが基本です。

<a href="#" data-ui="act" data-behavior="toggle" data-target="#menu">
  メニューをトグル
</a>
<div id="menu" hidden>メニュー内容</div>

2. <input type="button"> や <input type="submit"> の場合

<input> でも data-ui="act" を付ければ動作します。
ただし type="submit" の場合はフォーム送信が走るので、type="button" を明示しましょう。

<form>
  <input type="button" value="詳細を表示" data-ui="act" data-behavior="show" data-target="#detail">
</form>
<div id="detail" hidden>詳細内容</div>

3. <div> など任意の要素の場合

<div> をクリック可能にしてトリガーにすることも可能です。
その際は role="button" を付与して、キーボード操作にも対応させるとアクセシビリティ的に安心です。

<div role="button" tabindex="0" data-ui="act" data-behavior="toggle" data-target="#panel">
  パネルを開閉
</div>
<div id="panel" hidden>パネル内容</div>

👉 ポイントは 「意図しない既定動作を避ける」 こと。

  • <a> → ページ遷移を防ぐ
  • <input> → type="button" を使う
  • <div> → ARIA属性とキーボード対応を補足

これでボタン以外でも安心して使えます。


🔁 複数箇所で使うとき

イベント委譲で実装しているので、同じページ内で何度でも使えます。

例:FAQアコーディオンを複数並べる場合

<button type="button" data-ui="act" data-behavior="toggle" data-target="#faq1">
  Q1. 質問文
</button>
<div id="faq1" hidden>A1. 回答文</div>

<button type="button" data-ui="act" data-behavior="toggle" data-target="#faq2">
  Q2. 質問文
</button>
<div id="faq2" hidden>A2. 回答文</div>

→ それぞれ独立してトグル動作します。
モーダルやタブ切り替えなどにも応用可能です。


✅ まとめ

  • 初期状態は hidden 属性でシンプルに指定
  • JS側で .js-hidden に変換して統一管理
  • data-behaviordata-target だけで挙動を指定できる
  • 複数箇所でもそのまま流用可能
  • アクセシビリティも考慮済み

小さなUI制御を毎回書き直す手間を減らせるので、ぜひコピペして活用してください ✨


🔗 参考リンク

Discussion