アクションメニュー

カード右上の⋮ボタンから編集・複製・共有・削除をまとめた操作メニューを展開します。矢印キーでの移動、Escや外側クリックでの終了にも対応した基本のドロップダウンです。

#css#js#dropdown

ライブデモ

コード

HTML
<!-- アクションメニュー: カード右上の⋮ボタンをクリックすると編集/複製/共有/削除の操作一覧が展開する -->
<div class="dam-stage">
  <div class="dam-card">
    <div class="dam-card__thumb" aria-hidden="true">📄</div>
    <div class="dam-card__body">
      <p class="dam-card__title">四半期レポート.pdf</p>
      <p class="dam-card__meta">2026年7月2日に編集</p>
    </div>
    <div class="dam-menu">
      <button type="button" class="dam-trigger" id="damTrigger" aria-haspopup="true" aria-expanded="false" aria-controls="damList" aria-label="操作メニューを開く">
        <span class="dam-dots" aria-hidden="true">⋮</span>
      </button>
      <ul class="dam-list" id="damList" role="menu" aria-labelledby="damTrigger" hidden>
        <li role="none">
          <button type="button" class="dam-item" role="menuitem" data-action="edit">
            <span class="dam-item__icon" aria-hidden="true">✎</span>
            <span class="dam-item__label">編集</span>
          </button>
        </li>
        <li role="none">
          <button type="button" class="dam-item" role="menuitem" data-action="duplicate">
            <span class="dam-item__icon" aria-hidden="true">⧉</span>
            <span class="dam-item__label">複製</span>
          </button>
        </li>
        <li role="none">
          <button type="button" class="dam-item" role="menuitem" data-action="share">
            <span class="dam-item__icon" aria-hidden="true">↗</span>
            <span class="dam-item__label">共有</span>
          </button>
        </li>
        <li class="dam-sep" role="separator"></li>
        <li role="none">
          <button type="button" class="dam-item dam-item--danger" role="menuitem" data-action="delete">
            <span class="dam-item__icon" aria-hidden="true">🗑</span>
            <span class="dam-item__label">削除</span>
          </button>
        </li>
      </ul>
    </div>
  </div>
  <p class="dam-hint" id="damStatus" aria-live="polite">⋮ をクリックすると操作メニューが開きます。</p>
</div>
CSS
/* 落ち着いたスレート×インディゴの背景に浮かぶカード。メニューはカード右上の⋮から展開する */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background:
    radial-gradient(circle at 15% 10%, rgba(99, 102, 241, .14) 0%, transparent 50%),
    radial-gradient(circle at 85% 90%, rgba(56, 189, 248, .12) 0%, transparent 50%),
    #f4f5f9;
  color: #1e293b;
}

.dam-stage { width: 100%; max-width: 400px; display: grid; gap: 16px; justify-items: center; }

.dam-card {
  position: relative;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 18px 16px;
  background: #fff;
  border: 1px solid #e6e8f0;
  border-radius: 16px;
  box-shadow: 0 18px 40px -22px rgba(30, 41, 59, .28);
}

.dam-card__thumb {
  flex: none;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  font-size: 20px;
  border-radius: 12px;
  background: linear-gradient(150deg, #eef2ff, #e0f2fe);
}

.dam-card__body { flex: 1; min-width: 0; }
.dam-card__title {
  margin: 0;
  font-size: 14.5px;
  font-weight: 700;
  color: #1e293b;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dam-card__meta { margin: 3px 0 0; font-size: 12px; color: #8b93a5; }

.dam-menu { position: relative; flex: none; }

.dam-trigger {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: #64748b;
  cursor: pointer;
  transition: background .18s ease, color .18s ease;
}
.dam-trigger:hover { background: #f1f2f8; color: #1e293b; }
.dam-trigger:focus-visible { outline: 2px solid #6366f1; outline-offset: 2px; }
.dam-trigger[aria-expanded="true"] { background: #eef0f8; color: #4338ca; }
.dam-dots { font-size: 20px; line-height: 1; letter-spacing: 1px; }

.dam-list {
  position: absolute;
  z-index: 10;
  top: calc(100% + 8px);
  right: 0;
  margin: 0;
  padding: 6px;
  min-width: 176px;
  list-style: none;
  background: #fff;
  border: 1px solid #eaecf3;
  border-radius: 14px;
  box-shadow: 0 24px 48px -20px rgba(30, 41, 59, .35);
  transform-origin: top right;
  opacity: 0;
  transform: translateY(-6px) scale(.96);
  transition: opacity .16s ease, transform .16s ease;
}
.dam-list[hidden] { display: none; }
.dam-list.is-open { opacity: 1; transform: translateY(0) scale(1); }

.dam-sep { height: 1px; margin: 6px 4px; background: #eef0f6; list-style: none; }

.dam-item {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  font: inherit;
  font-size: 13.5px;
  color: #334155;
  background: none;
  border: none;
  border-radius: 9px;
  text-align: left;
  cursor: pointer;
  transition: background .14s ease, color .14s ease;
}
.dam-item:hover,
.dam-item.is-active { background: #f1f2f8; }
.dam-item:focus-visible { outline: 2px solid #6366f1; outline-offset: -2px; }
.dam-item__icon { flex: none; width: 18px; text-align: center; font-size: 14px; color: #7c83a3; }
.dam-item__label { flex: 1; }
.dam-item--danger { color: #dc2626; }
.dam-item--danger .dam-item__icon { color: #dc2626; }
.dam-item--danger:hover,
.dam-item--danger.is-active { background: #fef2f2; }

.dam-hint { margin: 0; font-size: 12.5px; color: #94a1b8; text-align: center; min-height: 1.4em; }

@media (prefers-reduced-motion: reduce) {
  .dam-list, .dam-trigger { transition: none; }
}
JavaScript
// アクションメニュー: ⋮ボタンで開閉し、矢印キー移動・Home/End・外側クリック/Escでの終了・focusの復帰に対応する
(() => {
  const trigger = document.getElementById('damTrigger');
  const list = document.getElementById('damList');
  const status = document.getElementById('damStatus');
  if (!trigger || !list || !status) return;

  const items = Array.from(list.querySelectorAll('.dam-item'));
  if (!items.length) return;

  const isOpen = () => trigger.getAttribute('aria-expanded') === 'true';

  const setActive = (index) => {
    items.forEach((it, i) => it.classList.toggle('is-active', i === index));
  };

  const open = () => {
    list.hidden = false;
    void list.offsetHeight; // 強制リフローでトランジションを確実に発火させる
    list.classList.add('is-open');
    trigger.setAttribute('aria-expanded', 'true');
    setActive(0);
    items[0].focus();
  };

  const close = (focusTrigger) => {
    list.hidden = true;
    list.classList.remove('is-open');
    trigger.setAttribute('aria-expanded', 'false');
    setActive(-1);
    if (focusTrigger) trigger.focus();
  };

  trigger.addEventListener('click', () => {
    if (isOpen()) close(false); else open();
  });

  trigger.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      open();
    }
  });

  const focusByIndex = (index) => {
    const len = items.length;
    const i = ((index % len) + len) % len;
    setActive(i);
    items[i].focus();
  };

  list.addEventListener('keydown', (e) => {
    const current = items.indexOf(document.activeElement);
    switch (e.key) {
      case 'ArrowDown': e.preventDefault(); focusByIndex(current + 1); break;
      case 'ArrowUp': e.preventDefault(); focusByIndex(current - 1); break;
      case 'Home': e.preventDefault(); focusByIndex(0); break;
      case 'End': e.preventDefault(); focusByIndex(items.length - 1); break;
      case 'Escape': e.preventDefault(); close(true); break;
      case 'Tab': close(false); break;
      default: break;
    }
  });

  items.forEach((item, index) => {
    item.addEventListener('click', () => {
      const label = item.querySelector('.dam-item__label');
      const text = label ? label.textContent : item.textContent.trim();
      status.textContent = `「${text}」を実行しました`;
      close(true);
    });
    item.addEventListener('mouseenter', () => setActive(index));
  });

  document.addEventListener('click', (e) => {
    if (isOpen() && !e.target.closest('.dam-menu')) close(false);
  });
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「アクションメニュー」の効果を追加してください。

# 追加してほしい効果
アクションメニュー(ツールチップ & メニュー)
カード右上の⋮ボタンから編集・複製・共有・削除をまとめた操作メニューを展開します。矢印キーでの移動、Escや外側クリックでの終了にも対応した基本のドロップダウンです。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- アクションメニュー: カード右上の⋮ボタンをクリックすると編集/複製/共有/削除の操作一覧が展開する -->
<div class="dam-stage">
  <div class="dam-card">
    <div class="dam-card__thumb" aria-hidden="true">📄</div>
    <div class="dam-card__body">
      <p class="dam-card__title">四半期レポート.pdf</p>
      <p class="dam-card__meta">2026年7月2日に編集</p>
    </div>
    <div class="dam-menu">
      <button type="button" class="dam-trigger" id="damTrigger" aria-haspopup="true" aria-expanded="false" aria-controls="damList" aria-label="操作メニューを開く">
        <span class="dam-dots" aria-hidden="true">⋮</span>
      </button>
      <ul class="dam-list" id="damList" role="menu" aria-labelledby="damTrigger" hidden>
        <li role="none">
          <button type="button" class="dam-item" role="menuitem" data-action="edit">
            <span class="dam-item__icon" aria-hidden="true">✎</span>
            <span class="dam-item__label">編集</span>
          </button>
        </li>
        <li role="none">
          <button type="button" class="dam-item" role="menuitem" data-action="duplicate">
            <span class="dam-item__icon" aria-hidden="true">⧉</span>
            <span class="dam-item__label">複製</span>
          </button>
        </li>
        <li role="none">
          <button type="button" class="dam-item" role="menuitem" data-action="share">
            <span class="dam-item__icon" aria-hidden="true">↗</span>
            <span class="dam-item__label">共有</span>
          </button>
        </li>
        <li class="dam-sep" role="separator"></li>
        <li role="none">
          <button type="button" class="dam-item dam-item--danger" role="menuitem" data-action="delete">
            <span class="dam-item__icon" aria-hidden="true">🗑</span>
            <span class="dam-item__label">削除</span>
          </button>
        </li>
      </ul>
    </div>
  </div>
  <p class="dam-hint" id="damStatus" aria-live="polite">⋮ をクリックすると操作メニューが開きます。</p>
</div>

【CSS】
/* 落ち着いたスレート×インディゴの背景に浮かぶカード。メニューはカード右上の⋮から展開する */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background:
    radial-gradient(circle at 15% 10%, rgba(99, 102, 241, .14) 0%, transparent 50%),
    radial-gradient(circle at 85% 90%, rgba(56, 189, 248, .12) 0%, transparent 50%),
    #f4f5f9;
  color: #1e293b;
}

.dam-stage { width: 100%; max-width: 400px; display: grid; gap: 16px; justify-items: center; }

.dam-card {
  position: relative;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 18px 16px;
  background: #fff;
  border: 1px solid #e6e8f0;
  border-radius: 16px;
  box-shadow: 0 18px 40px -22px rgba(30, 41, 59, .28);
}

.dam-card__thumb {
  flex: none;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  font-size: 20px;
  border-radius: 12px;
  background: linear-gradient(150deg, #eef2ff, #e0f2fe);
}

.dam-card__body { flex: 1; min-width: 0; }
.dam-card__title {
  margin: 0;
  font-size: 14.5px;
  font-weight: 700;
  color: #1e293b;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dam-card__meta { margin: 3px 0 0; font-size: 12px; color: #8b93a5; }

.dam-menu { position: relative; flex: none; }

.dam-trigger {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: #64748b;
  cursor: pointer;
  transition: background .18s ease, color .18s ease;
}
.dam-trigger:hover { background: #f1f2f8; color: #1e293b; }
.dam-trigger:focus-visible { outline: 2px solid #6366f1; outline-offset: 2px; }
.dam-trigger[aria-expanded="true"] { background: #eef0f8; color: #4338ca; }
.dam-dots { font-size: 20px; line-height: 1; letter-spacing: 1px; }

.dam-list {
  position: absolute;
  z-index: 10;
  top: calc(100% + 8px);
  right: 0;
  margin: 0;
  padding: 6px;
  min-width: 176px;
  list-style: none;
  background: #fff;
  border: 1px solid #eaecf3;
  border-radius: 14px;
  box-shadow: 0 24px 48px -20px rgba(30, 41, 59, .35);
  transform-origin: top right;
  opacity: 0;
  transform: translateY(-6px) scale(.96);
  transition: opacity .16s ease, transform .16s ease;
}
.dam-list[hidden] { display: none; }
.dam-list.is-open { opacity: 1; transform: translateY(0) scale(1); }

.dam-sep { height: 1px; margin: 6px 4px; background: #eef0f6; list-style: none; }

.dam-item {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  font: inherit;
  font-size: 13.5px;
  color: #334155;
  background: none;
  border: none;
  border-radius: 9px;
  text-align: left;
  cursor: pointer;
  transition: background .14s ease, color .14s ease;
}
.dam-item:hover,
.dam-item.is-active { background: #f1f2f8; }
.dam-item:focus-visible { outline: 2px solid #6366f1; outline-offset: -2px; }
.dam-item__icon { flex: none; width: 18px; text-align: center; font-size: 14px; color: #7c83a3; }
.dam-item__label { flex: 1; }
.dam-item--danger { color: #dc2626; }
.dam-item--danger .dam-item__icon { color: #dc2626; }
.dam-item--danger:hover,
.dam-item--danger.is-active { background: #fef2f2; }

.dam-hint { margin: 0; font-size: 12.5px; color: #94a1b8; text-align: center; min-height: 1.4em; }

@media (prefers-reduced-motion: reduce) {
  .dam-list, .dam-trigger { transition: none; }
}

【JavaScript】
// アクションメニュー: ⋮ボタンで開閉し、矢印キー移動・Home/End・外側クリック/Escでの終了・focusの復帰に対応する
(() => {
  const trigger = document.getElementById('damTrigger');
  const list = document.getElementById('damList');
  const status = document.getElementById('damStatus');
  if (!trigger || !list || !status) return;

  const items = Array.from(list.querySelectorAll('.dam-item'));
  if (!items.length) return;

  const isOpen = () => trigger.getAttribute('aria-expanded') === 'true';

  const setActive = (index) => {
    items.forEach((it, i) => it.classList.toggle('is-active', i === index));
  };

  const open = () => {
    list.hidden = false;
    void list.offsetHeight; // 強制リフローでトランジションを確実に発火させる
    list.classList.add('is-open');
    trigger.setAttribute('aria-expanded', 'true');
    setActive(0);
    items[0].focus();
  };

  const close = (focusTrigger) => {
    list.hidden = true;
    list.classList.remove('is-open');
    trigger.setAttribute('aria-expanded', 'false');
    setActive(-1);
    if (focusTrigger) trigger.focus();
  };

  trigger.addEventListener('click', () => {
    if (isOpen()) close(false); else open();
  });

  trigger.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      open();
    }
  });

  const focusByIndex = (index) => {
    const len = items.length;
    const i = ((index % len) + len) % len;
    setActive(i);
    items[i].focus();
  };

  list.addEventListener('keydown', (e) => {
    const current = items.indexOf(document.activeElement);
    switch (e.key) {
      case 'ArrowDown': e.preventDefault(); focusByIndex(current + 1); break;
      case 'ArrowUp': e.preventDefault(); focusByIndex(current - 1); break;
      case 'Home': e.preventDefault(); focusByIndex(0); break;
      case 'End': e.preventDefault(); focusByIndex(items.length - 1); break;
      case 'Escape': e.preventDefault(); close(true); break;
      case 'Tab': close(false); break;
      default: break;
    }
  });

  items.forEach((item, index) => {
    item.addEventListener('click', () => {
      const label = item.querySelector('.dam-item__label');
      const text = label ? label.textContent : item.textContent.trim();
      status.textContent = `「${text}」を実行しました`;
      close(true);
    });
    item.addEventListener('mouseenter', () => setActive(index));
  });

  document.addEventListener('click', (e) => {
    if (isOpen() && !e.target.closest('.dam-menu')) close(false);
  });
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。