右クリックメニュー

枠内を右クリックするとカーソル位置にコピーや削除などの操作メニューが開きます。画面端では自動的に位置を反転させ、はみ出さずに収まるようにしています。

#css#js#menu

ライブデモ

コード

HTML
<!-- 右クリックメニュー: 枠内で右クリックするとカーソル位置に開き、画面端では自動的に反転して収まる -->
<div class="dcm-stage">
  <div class="dcm-pad" id="dcmPad" tabindex="0">
    <p class="dcm-pad__title">この枠内を右クリック</p>
    <p class="dcm-pad__sub">画像やテキストを右クリックした時の操作メニューを想定したデモです。フォーカスして Shift+F10 でも開けます。</p>
  </div>
  <p class="dcm-hint" id="dcmStatus" aria-live="polite"></p>

  <ul class="dcm-menu" id="dcmMenu" role="menu" hidden>
    <li role="none">
      <button type="button" class="dcm-item" role="menuitem" data-action="copy">
        <span class="dcm-item__icon" aria-hidden="true">⧉</span>
        <span class="dcm-item__label">コピー</span>
        <span class="dcm-item__key" aria-hidden="true">Ctrl+C</span>
      </button>
    </li>
    <li role="none">
      <button type="button" class="dcm-item" role="menuitem" data-action="paste">
        <span class="dcm-item__icon" aria-hidden="true">📋</span>
        <span class="dcm-item__label">貼り付け</span>
        <span class="dcm-item__key" aria-hidden="true">Ctrl+V</span>
      </button>
    </li>
    <li class="dcm-sep" role="separator"></li>
    <li role="none">
      <button type="button" class="dcm-item" role="menuitem" data-action="save">
        <span class="dcm-item__icon" aria-hidden="true">⬇</span>
        <span class="dcm-item__label">名前を付けて保存</span>
      </button>
    </li>
    <li role="none">
      <button type="button" class="dcm-item" role="menuitem" data-action="inspect">
        <span class="dcm-item__icon" aria-hidden="true">🔍</span>
        <span class="dcm-item__label">検証</span>
      </button>
    </li>
    <li class="dcm-sep" role="separator"></li>
    <li role="none">
      <button type="button" class="dcm-item dcm-item--danger" role="menuitem" data-action="delete">
        <span class="dcm-item__icon" aria-hidden="true">🗑</span>
        <span class="dcm-item__label">削除</span>
      </button>
    </li>
  </ul>
</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 12% 8%, rgba(59, 130, 246, .08) 0%, transparent 45%),
    radial-gradient(circle at 88% 92%, rgba(14, 165, 233, .08) 0%, transparent 45%),
    #f7f8fb;
  color: #1e2433;
}

.dcm-stage { width: 100%; max-width: 440px; display: grid; gap: 14px; justify-items: center; }

.dcm-pad {
  width: 100%;
  padding: 36px 26px;
  text-align: center;
  background: #fff;
  border: 1.5px dashed #cbd5e1;
  border-radius: 18px;
  cursor: context-menu;
  user-select: none;
  transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
}
.dcm-pad:focus-visible { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 4px rgba(59, 130, 246, .14); }
.dcm-pad.is-context-active { border-style: solid; border-color: #3b82f6; background: #f5f9ff; }

.dcm-pad__title { margin: 0 0 6px; font-size: 15px; font-weight: 700; color: #1e2433; }
.dcm-pad__sub { margin: 0; font-size: 12.5px; color: #8892a3; line-height: 1.7; }

.dcm-hint { margin: 0; min-height: 1.4em; font-size: 12.5px; color: #64748b; text-align: center; }

.dcm-menu {
  position: fixed;
  z-index: 50;
  margin: 0;
  padding: 6px;
  min-width: 208px;
  list-style: none;
  background: #fff;
  border: 1px solid #e6e9f0;
  border-radius: 12px;
  box-shadow: 0 26px 54px -18px rgba(15, 23, 42, .35);
  opacity: 0;
  transform: scale(.96);
  transition: opacity .13s ease, transform .13s ease;
}
.dcm-menu[hidden] { display: none; }
.dcm-menu.is-open { opacity: 1; transform: scale(1); }

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

.dcm-item {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  font: inherit;
  font-size: 13px;
  color: #2a3242;
  background: none;
  border: none;
  border-radius: 8px;
  text-align: left;
  cursor: pointer;
  transition: background .12s ease;
}
.dcm-item:hover,
.dcm-item.is-active { background: #eff4ff; }
.dcm-item:focus-visible { outline: 2px solid #3b82f6; outline-offset: -2px; }
.dcm-item__icon { flex: none; width: 16px; text-align: center; font-size: 13px; color: #64748b; }
.dcm-item__label { flex: 1; }
.dcm-item__key { flex: none; font-size: 11px; color: #9aa4b5; }
.dcm-item--danger { color: #dc2626; }
.dcm-item--danger .dcm-item__icon { color: #dc2626; }
.dcm-item--danger:hover,
.dcm-item--danger.is-active { background: #fef2f2; }

@media (prefers-reduced-motion: reduce) {
  .dcm-menu, .dcm-pad { transition: none; }
}
JavaScript
// 右クリックメニュー: contextmenuイベントを奪ってカーソル位置に開き、画面端では反転させて収める
(() => {
  const pad = document.getElementById('dcmPad');
  const menu = document.getElementById('dcmMenu');
  const status = document.getElementById('dcmStatus');
  if (!pad || !menu || !status) return;

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

  const isOpen = () => menu.classList.contains('is-open');

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

  const openMenuAt = (x, y) => {
    menu.hidden = false;
    menu.style.left = '0px';
    menu.style.top = '0px';
    void menu.offsetHeight; // 強制リフローで実測サイズを取得してから配置する
    const margin = 8;
    const mw = menu.offsetWidth;
    const mh = menu.offsetHeight;
    let left = x;
    let top = y;
    if (left + mw + margin > window.innerWidth) left = x - mw;
    if (top + mh + margin > window.innerHeight) top = y - mh;
    left = Math.max(margin, left);
    top = Math.max(margin, top);
    menu.style.left = `${left}px`;
    menu.style.top = `${top}px`;
    menu.classList.add('is-open');
    pad.classList.add('is-context-active');
    setActive(0);
    items[0].focus();
  };

  const closeMenu = (focusPad) => {
    menu.hidden = true;
    menu.classList.remove('is-open');
    pad.classList.remove('is-context-active');
    setActive(-1);
    if (focusPad) pad.focus();
  };

  pad.addEventListener('contextmenu', (e) => {
    e.preventDefault();
    openMenuAt(e.clientX, e.clientY);
  });

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

  menu.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(); closeMenu(true); break;
      case 'Tab': closeMenu(false); break;
      default: break;
    }
  });

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

  document.addEventListener('click', (e) => {
    if (isOpen() && !menu.contains(e.target)) closeMenu(false);
  });
  document.addEventListener('contextmenu', (e) => {
    if (isOpen() && !pad.contains(e.target)) closeMenu(false);
  });
  window.addEventListener('scroll', () => {
    if (isOpen()) closeMenu(false);
  }, { passive: true });
})();

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

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

# 追加してほしい効果
右クリックメニュー(ツールチップ & メニュー)
枠内を右クリックするとカーソル位置にコピーや削除などの操作メニューが開きます。画面端では自動的に位置を反転させ、はみ出さずに収まるようにしています。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 右クリックメニュー: 枠内で右クリックするとカーソル位置に開き、画面端では自動的に反転して収まる -->
<div class="dcm-stage">
  <div class="dcm-pad" id="dcmPad" tabindex="0">
    <p class="dcm-pad__title">この枠内を右クリック</p>
    <p class="dcm-pad__sub">画像やテキストを右クリックした時の操作メニューを想定したデモです。フォーカスして Shift+F10 でも開けます。</p>
  </div>
  <p class="dcm-hint" id="dcmStatus" aria-live="polite"></p>

  <ul class="dcm-menu" id="dcmMenu" role="menu" hidden>
    <li role="none">
      <button type="button" class="dcm-item" role="menuitem" data-action="copy">
        <span class="dcm-item__icon" aria-hidden="true">⧉</span>
        <span class="dcm-item__label">コピー</span>
        <span class="dcm-item__key" aria-hidden="true">Ctrl+C</span>
      </button>
    </li>
    <li role="none">
      <button type="button" class="dcm-item" role="menuitem" data-action="paste">
        <span class="dcm-item__icon" aria-hidden="true">📋</span>
        <span class="dcm-item__label">貼り付け</span>
        <span class="dcm-item__key" aria-hidden="true">Ctrl+V</span>
      </button>
    </li>
    <li class="dcm-sep" role="separator"></li>
    <li role="none">
      <button type="button" class="dcm-item" role="menuitem" data-action="save">
        <span class="dcm-item__icon" aria-hidden="true">⬇</span>
        <span class="dcm-item__label">名前を付けて保存</span>
      </button>
    </li>
    <li role="none">
      <button type="button" class="dcm-item" role="menuitem" data-action="inspect">
        <span class="dcm-item__icon" aria-hidden="true">🔍</span>
        <span class="dcm-item__label">検証</span>
      </button>
    </li>
    <li class="dcm-sep" role="separator"></li>
    <li role="none">
      <button type="button" class="dcm-item dcm-item--danger" role="menuitem" data-action="delete">
        <span class="dcm-item__icon" aria-hidden="true">🗑</span>
        <span class="dcm-item__label">削除</span>
      </button>
    </li>
  </ul>
</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 12% 8%, rgba(59, 130, 246, .08) 0%, transparent 45%),
    radial-gradient(circle at 88% 92%, rgba(14, 165, 233, .08) 0%, transparent 45%),
    #f7f8fb;
  color: #1e2433;
}

.dcm-stage { width: 100%; max-width: 440px; display: grid; gap: 14px; justify-items: center; }

.dcm-pad {
  width: 100%;
  padding: 36px 26px;
  text-align: center;
  background: #fff;
  border: 1.5px dashed #cbd5e1;
  border-radius: 18px;
  cursor: context-menu;
  user-select: none;
  transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
}
.dcm-pad:focus-visible { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 4px rgba(59, 130, 246, .14); }
.dcm-pad.is-context-active { border-style: solid; border-color: #3b82f6; background: #f5f9ff; }

.dcm-pad__title { margin: 0 0 6px; font-size: 15px; font-weight: 700; color: #1e2433; }
.dcm-pad__sub { margin: 0; font-size: 12.5px; color: #8892a3; line-height: 1.7; }

.dcm-hint { margin: 0; min-height: 1.4em; font-size: 12.5px; color: #64748b; text-align: center; }

.dcm-menu {
  position: fixed;
  z-index: 50;
  margin: 0;
  padding: 6px;
  min-width: 208px;
  list-style: none;
  background: #fff;
  border: 1px solid #e6e9f0;
  border-radius: 12px;
  box-shadow: 0 26px 54px -18px rgba(15, 23, 42, .35);
  opacity: 0;
  transform: scale(.96);
  transition: opacity .13s ease, transform .13s ease;
}
.dcm-menu[hidden] { display: none; }
.dcm-menu.is-open { opacity: 1; transform: scale(1); }

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

.dcm-item {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  font: inherit;
  font-size: 13px;
  color: #2a3242;
  background: none;
  border: none;
  border-radius: 8px;
  text-align: left;
  cursor: pointer;
  transition: background .12s ease;
}
.dcm-item:hover,
.dcm-item.is-active { background: #eff4ff; }
.dcm-item:focus-visible { outline: 2px solid #3b82f6; outline-offset: -2px; }
.dcm-item__icon { flex: none; width: 16px; text-align: center; font-size: 13px; color: #64748b; }
.dcm-item__label { flex: 1; }
.dcm-item__key { flex: none; font-size: 11px; color: #9aa4b5; }
.dcm-item--danger { color: #dc2626; }
.dcm-item--danger .dcm-item__icon { color: #dc2626; }
.dcm-item--danger:hover,
.dcm-item--danger.is-active { background: #fef2f2; }

@media (prefers-reduced-motion: reduce) {
  .dcm-menu, .dcm-pad { transition: none; }
}

【JavaScript】
// 右クリックメニュー: contextmenuイベントを奪ってカーソル位置に開き、画面端では反転させて収める
(() => {
  const pad = document.getElementById('dcmPad');
  const menu = document.getElementById('dcmMenu');
  const status = document.getElementById('dcmStatus');
  if (!pad || !menu || !status) return;

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

  const isOpen = () => menu.classList.contains('is-open');

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

  const openMenuAt = (x, y) => {
    menu.hidden = false;
    menu.style.left = '0px';
    menu.style.top = '0px';
    void menu.offsetHeight; // 強制リフローで実測サイズを取得してから配置する
    const margin = 8;
    const mw = menu.offsetWidth;
    const mh = menu.offsetHeight;
    let left = x;
    let top = y;
    if (left + mw + margin > window.innerWidth) left = x - mw;
    if (top + mh + margin > window.innerHeight) top = y - mh;
    left = Math.max(margin, left);
    top = Math.max(margin, top);
    menu.style.left = `${left}px`;
    menu.style.top = `${top}px`;
    menu.classList.add('is-open');
    pad.classList.add('is-context-active');
    setActive(0);
    items[0].focus();
  };

  const closeMenu = (focusPad) => {
    menu.hidden = true;
    menu.classList.remove('is-open');
    pad.classList.remove('is-context-active');
    setActive(-1);
    if (focusPad) pad.focus();
  };

  pad.addEventListener('contextmenu', (e) => {
    e.preventDefault();
    openMenuAt(e.clientX, e.clientY);
  });

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

  menu.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(); closeMenu(true); break;
      case 'Tab': closeMenu(false); break;
      default: break;
    }
  });

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

  document.addEventListener('click', (e) => {
    if (isOpen() && !menu.contains(e.target)) closeMenu(false);
  });
  document.addEventListener('contextmenu', (e) => {
    if (isOpen() && !pad.contains(e.target)) closeMenu(false);
  });
  window.addEventListener('scroll', () => {
    if (isOpen()) closeMenu(false);
  }, { passive: true });
})();

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

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