状態表示

状態を切り替えられる通知リスト

通常、0件、エラーの表示を切り替え、テーマや表示幅による見え方の違いを試せる通知リストです。

動きを試す

プレビュー

プレビュー設定

プレビューを準備しています。

必要なファイルを選ぶ

コード

preview.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>
    <style>
      :root {
        color-scheme: light;
        --bg: #f7f8fa;
        --surface: #ffffff;
        --surface-subtle: #f0f3f7;
        --text: #18212f;
        --muted: #5d6b7a;
        --border: #d7dee8;
        --accent: #245fd6;
        --accent-strong: #1849a9;
        --danger: #b42318;
        --focus: #0077cc;
        font-family:
          -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans JP", sans-serif;
      }

      :root[data-theme="dark"] {
        color-scheme: dark;
        --bg: #0f141a;
        --surface: #171d26;
        --surface-subtle: #202834;
        --text: #f2f5f8;
        --muted: #aab4c0;
        --border: #354152;
        --accent: #7ea7ff;
        --accent-strong: #a8c2ff;
        --danger: #ff8a80;
        --focus: #65c7ff;
      }

      * {
        box-sizing: border-box;
      }

      html {
        min-width: 0;
        background: var(--bg);
      }

      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;
      }

      .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: 700;
        letter-spacing: 0.04em;
      }

      h1,
      h2,
      p {
        overflow-wrap: anywhere;
      }

      h1 {
        margin: 0;
        font-size: clamp(1.35rem, 5vw, 2rem);
        line-height: 1.3;
      }

      .lead {
        margin: 8px 0 20px;
        color: var(--muted);
      }

      [data-state-panel][hidden] {
        display: none;
      }

      .state-panel {
        min-width: 0;
        padding: 16px;
        border: 1px solid var(--border);
        border-radius: 8px;
        background: var(--surface-subtle);
      }

      .state-panel h2 {
        margin: 0 0 8px;
        font-size: 1.125rem;
      }

      .state-panel p {
        margin: 0;
      }

      .item-list {
        display: grid;
        gap: 10px;
        margin: 12px 0 0;
        padding: 0;
        list-style: none;
      }

      .item-list li {
        min-width: 0;
        padding: 12px;
        border: 1px solid var(--border);
        border-radius: 8px;
        background: var(--surface);
      }

      .item-list strong,
      .item-list span {
        display: block;
        overflow-wrap: anywhere;
      }

      .item-list span {
        color: var(--muted);
        font-size: 0.9rem;
      }

      .error-title {
        color: var(--danger);
      }

      button {
        min-height: 44px;
        margin-top: 14px;
        padding: 10px 16px;
        border: 1px solid var(--accent);
        border-radius: 8px;
        color: #ffffff;
        background: var(--accent);
        font: inherit;
        font-weight: 700;
        cursor: pointer;
      }

      :root[data-theme="dark"] button {
        color: #0f141a;
      }

      button:hover {
        background: var(--accent-strong);
      }

      button:focus-visible {
        outline: 3px solid var(--focus);
        outline-offset: 3px;
      }

      .status {
        min-height: 1.65em;
        margin: 14px 0 0;
        color: var(--muted);
        font-size: 0.9rem;
      }

      @media (max-width: 360px) {
        body {
          padding: 10px;
        }

        .panel {
          padding: 14px;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        *,
        *::before,
        *::after {
          scroll-behavior: auto !important;
          animation-duration: 0.01ms !important;
          animation-iteration-count: 1 !important;
          transition-duration: 0.01ms !important;
        }
      }
    </style>
  </head>
  <body>
    <main>
      <article class="panel" aria-labelledby="preview-title">
        <p class="eyebrow">通知リストの表示例</p>
        <h1 id="preview-title">通知項目</h1>
        <p class="lead">通常、0件、エラーの表示を切り替えて確認できます。</p>

        <section class="state-panel" data-state-panel="default" aria-labelledby="default-title">
          <h2 id="default-title">3件の項目があります</h2>
          <ul class="item-list">
            <li>
              <strong>入力内容を確認してください</strong>
              <span>確認が必要な項目です</span>
            </li>
            <li>
              <strong>長い日本語見出しでも領域全体が横へ広がらないことを確認する項目</strong>
              <span>識別子: SAMPLE-2026-08-22-ALPHA-0001</span>
            </li>
            <li>
              <strong>次の操作を選択できます</strong>
              <span>表示内容は上の設定から切り替えられます。</span>
            </li>
          </ul>
        </section>

        <section
          class="state-panel"
          data-state-panel="empty"
          aria-labelledby="empty-title"
          hidden
        >
          <h2 id="empty-title">表示できる項目はありません</h2>
          <p>条件を変更するか、時間を置いて再度確認してください。</p>
        </section>

        <section
          class="state-panel"
          data-state-panel="error"
          aria-labelledby="error-title"
          hidden
        >
          <h2 id="error-title" class="error-title">項目を表示できませんでした</h2>
          <p>一時的な問題の可能性があります。再試行して通常状態へ戻せます。</p>
          <button type="button" data-retry>再試行する</button>
        </section>

        <p class="status" role="status" aria-live="polite" data-status></p>
      </article>
    </main>

    <script>
      (() => {
        const params = new URLSearchParams(window.location.search);
        const instance = params.get("instance") ?? "missing-instance";
        const panels = Array.from(document.querySelectorAll("[data-state-panel]"));
        const status = document.querySelector("[data-status]");
        const retry = document.querySelector("[data-retry]");
        const validStates = new Set(["default", "empty", "error"]);
        const validThemes = new Set(["light", "dark"]);
        let currentState = validStates.has(params.get("state")) ? params.get("state") : "default";

        const announce = (message) => {
          if (status instanceof HTMLElement) {
            status.textContent = message;
          }
        };

        const setState = (nextState, shouldAnnounce = true) => {
          if (!nextState || !validStates.has(nextState)) {
            return;
          }
          currentState = nextState;
          for (const panel of panels) {
            if (panel instanceof HTMLElement) {
              panel.hidden = panel.dataset.statePanel !== currentState;
            }
          }
          document.documentElement.dataset.state = currentState;
          if (shouldAnnounce) {
            const label = currentState === "default" ? "通常" : currentState === "empty" ? "0件" : "失敗";
            announce(`${label}状態へ切り替えました。`);
          }
        };

        const setTheme = (theme) => {
          if (!theme || !validThemes.has(theme)) {
            return;
          }
          document.documentElement.dataset.theme = theme;
        };

        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") {
            setState(typeof data.value === "string" ? data.value : null);
          }
          if (data.type === "wpl-preview-theme") {
            setTheme(typeof data.value === "string" ? data.value : null);
          }
        });

        retry?.addEventListener("click", () => {
          if (!(retry instanceof HTMLButtonElement)) {
            return;
          }
          retry.disabled = true;
          retry.textContent = "再試行中…";
          window.setTimeout(() => {
            retry.disabled = false;
            retry.textContent = "再試行する";
            setState("default");
          }, 250);
        });

        setTheme(validThemes.has(params.get("theme")) ? params.get("theme") : "light");
        setState(currentState, false);

        window.parent.postMessage(
          {
            source: "wpl-preview",
            type: "wpl-preview-ready",
            instance
          },
          "*"
        );
      })();
    </script>
  </body>
</html>
プレビューで試せる動き
  • 通常: 3件の中立的な項目を表示します。
  • 0件: 正常に取得できたが表示対象がない状態です。
  • 失敗: 一時的な失敗と再試行操作を表示します。

使い分けを確認する

このレシピが合う場面

適する場面

  • お知らせや対応項目を一覧で表示する
  • 0件や一時的な失敗から次の行動を案内する
  • ライト・ダークと複数の表示幅へ対応する

適さない場面

  • 大量の項目を検索・並べ替え・ページ分割する必要がある場合
  • リアルタイム更新や複雑な通知設定が必要な場合