コンテンツ
開閉できる補足セクション
必須ではない補足を見出し単位で開閉し、複数項目を開いたまま比較できるnative disclosureレシピです。
動きを試す
プレビュー
必要なファイルを選ぶ
コード
index.html
利用前に変更する箇所
- 見出しと内容を実装先の任意補足へ置き換え、必須情報を外側へ残す
<!doctype html>
<html lang="ja" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>開閉できる補足セクション</title>
<link rel="stylesheet" href="./styles.css" />
<script src="./states.js" defer></script>
</head>
<body>
<main>
<section class="disclosure-panel" aria-labelledby="disclosure-title" data-disclosure-root>
<header>
<p class="eyebrow">補足情報の表示例</p>
<h1 id="disclosure-title">実装前に確認すること</h1>
<p class="lead">必要な項目を複数開いたまま比較できます。重要な利用条件はこの外側へ表示します。</p>
</header>
<div class="disclosure-list">
<details data-section="data">
<summary>表示するデータの境界</summary>
<div class="section-content">
<p>短い値だけでなく、長い日本語、0件、英数字が連続する値を用意します。</p>
<ul>
<li>0件を失敗と混同しない</li>
<li>長い値を固定高で切らない</li>
</ul>
</div>
</details>
<details data-section="interaction">
<summary>操作できない状態の扱い</summary>
<div class="section-content">
<p>処理中や無効状態では、操作できない理由と次に可能な行動を近くへ表示します。</p>
</div>
</details>
<details data-section="long-content">
<summary>長い識別子と補足文の確認</summary>
<div class="section-content">
<p>
画面幅を超えやすい値
SAMPLE-CONTENT-SECTION-2026-08-24-ALPHA-0000000000000001
と、複数行になる日本語説明を同じ領域で確認します。
</p>
</div>
</details>
</div>
<p class="status" role="status" aria-live="polite" data-status></p>
</section>
</main>
</body>
</html>
styles.css
利用前に変更する箇所
- 色tokenと余白を実装先のデザインシステムへ接続する
:root {
color-scheme: light;
--bg: #f4f7f8;
--surface: #ffffff;
--surface-subtle: #e9f0f2;
--text: #152129;
--muted: #53656f;
--border: #c6d3d8;
--accent: #006e82;
--accent-strong: #004f60;
--focus: #007c92;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans JP", sans-serif;
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #0b1117;
--surface: #111a21;
--surface-subtle: #19252e;
--text: #edf5f7;
--muted: #a8b8c0;
--border: #33434e;
--accent: #35c9e8;
--accent-strong: #70def2;
--focus: #5adaf2;
}
* {
box-sizing: border-box;
}
body {
min-width: 0;
margin: 0;
padding: clamp(12px, 4vw, 28px);
color: var(--text);
background: var(--bg);
font-size: 16px;
line-height: 1.65;
}
main {
width: min(100%, 720px);
margin-inline: auto;
}
.disclosure-panel {
min-width: 0;
padding: clamp(16px, 4vw, 28px);
border: 1px solid var(--border);
border-radius: 12px;
background: var(--surface);
}
.eyebrow {
margin: 0 0 4px;
color: var(--muted);
font-size: 0.875rem;
font-weight: 800;
}
h1 {
margin: 0;
font-size: clamp(1.4rem, 5vw, 2rem);
}
.lead {
margin: 8px 0 20px;
color: var(--muted);
}
.disclosure-list {
display: grid;
gap: 10px;
}
details {
min-width: 0;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-subtle);
}
details[open] {
border-color: var(--accent);
}
summary {
min-height: 48px;
padding: 12px 44px 12px 14px;
color: var(--text);
font-weight: 800;
cursor: pointer;
overflow-wrap: anywhere;
}
summary::marker {
color: var(--accent);
}
summary:hover {
color: var(--accent-strong);
}
summary:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 3px;
}
.section-content {
padding: 0 14px 14px;
border-top: 1px solid var(--border);
}
.section-content p {
margin: 12px 0 0;
overflow-wrap: anywhere;
}
.section-content ul {
margin: 10px 0 0;
padding-left: 1.25rem;
}
.status {
min-height: 1.6em;
margin: 14px 0 0;
color: var(--muted);
font-size: 0.875rem;
}
@media (max-width: 360px) {
body {
padding: 10px;
}
.disclosure-panel {
padding: 14px;
}
summary {
padding-inline: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto;
transition-duration: 0.01ms;
}
}
states.js
利用前に変更する箇所
- preview用の固定状態切替が不要ならhost連携部分を削除できる
(() => {
const params = new URLSearchParams(window.location.search);
const instance = params.get("instance") ?? "missing-instance";
const validStates = new Set([
"all-collapsed",
"first-expanded",
"multiple-expanded",
"long-content",
]);
const validThemes = new Set(["light", "dark"]);
const sections = Array.from(document.querySelectorAll("details[data-section]"));
const status = document.querySelector("[data-status]");
function applyState(state, announce = true) {
if (!validStates.has(state)) {
return;
}
const openSections = {
"all-collapsed": [],
"first-expanded": ["data"],
"multiple-expanded": ["data", "interaction"],
"long-content": ["long-content"],
}[state];
for (const section of sections) {
if (section instanceof HTMLDetailsElement) {
section.open = openSections.includes(section.dataset.section);
}
}
document.documentElement.dataset.state = state;
if (announce && status instanceof HTMLElement) {
status.textContent = `${openSections.length}件の補足を開いています。`;
}
}
function setTheme(theme) {
if (validThemes.has(theme)) {
document.documentElement.dataset.theme = theme;
}
}
for (const section of sections) {
section.addEventListener("toggle", () => {
if (status instanceof HTMLElement) {
const count = sections.filter(
(candidate) => candidate instanceof HTMLDetailsElement && candidate.open,
).length;
status.textContent = `${count}件の補足を開いています。`;
}
});
}
window.addEventListener("message", (event) => {
if (event.source !== window.parent || typeof event.data !== "object" || event.data === null) {
return;
}
const data = event.data;
if (data.source !== "wpl-host" || data.instance !== instance) {
return;
}
if (data.type === "wpl-preview-state") {
applyState(typeof data.value === "string" ? data.value : "");
}
if (data.type === "wpl-preview-theme") {
setTheme(typeof data.value === "string" ? data.value : "");
}
});
setTheme(validThemes.has(params.get("theme")) ? params.get("theme") : "light");
applyState(validStates.has(params.get("state")) ? params.get("state") : "all-collapsed", false);
window.parent.postMessage({ source: "wpl-preview", type: "wpl-preview-ready", instance }, "*");
})();
プレビューで試せる動き
- すべて閉じる: 補足をすべて閉じた初期状態です。
- 先頭を開く: 先頭の補足だけを表示します。
- 複数を開く: 複数の補足を同時に比較します。
- 長文を開く: 長い日本語と改行しにくい識別子を確認します。
使い分けを確認する
このレシピが合う場面
適する場面
- 同じ主題に属する複数の補足情報
- 利用者が複数項目を同時に開いて比較する説明
- 初期表示の文量を抑えても判断を誤らせない補助領域
適さない場面
- 全員が必ず読む警告や利用条件
- 入力手順を隠して分割する場合
- 開閉領域を入れ子にする場合
