🛠️
hidden属性+data属性だけでUIを制御する汎用JavaScript
🎯 背景
Web制作をしていると「Aを押したらBが出る/消える」といったシンプルなUI制御はとてもよく出てきます。
毎回個別にJSを書くのは面倒なので、data属性だけで汎用的に扱えるJS を備忘録としてまとめました。
🚀 仕様
-
初期状態の管理はHTML+CSS(非表示は
hidden
属性) -
挙動制御はすべて
data-
属性で指定 - JSはクリックイベントを拾うだけの最小構成
- 要素が存在しなくてもエラーにならない安全設計
- アクセシビリティ対応(
aria-hidden
とinert
)
🛠 データ属性の設計
-
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"
を持つ要素」なら何でもトリガーにできます。
ただし要素の種類によって気をつける点があります。
<a>
タグの場合
1. 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-behavior
とdata-target
だけで挙動を指定できる - 複数箇所でもそのまま流用可能
- アクセシビリティも考慮済み
小さなUI制御を毎回書き直す手間を減らせるので、ぜひコピペして活用してください ✨
Discussion