Explicación

Un fragmento de interfaz de usuario que anula el menú contextual predeterminado (menú contextual) y muestra un menú diseñado a medida.

Características

Puntos de implementación

Al usar getBoundingClientRect() en el cálculo de la posición y comparar el tamaño de la ventana del navegador y las coordenadas en las que se hizo clic, controlamos el menú para que no sobresalga de la pantalla.

Right click anywhere in this area.

<div class="context-area">
  <div class="context-message">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
      <line x1="9" y1="3" x2="9" y2="21"></line>
    </svg>
    <p>Right click anywhere in this area.</p>
  </div>
</div>

<div class="custom-context-menu" id="context-menu">
  <ul class="menu-list">
    <li class="menu-item">
      <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="15 18 9 12 15 6"></polyline>
      </svg>
      <span>Back</span>
      <span class="menu-shortcut">Alt+Left</span>
    </li>
    <li class="menu-item">
      <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="9 18 15 12 9 6"></polyline>
      </svg>
      <span>Forward</span>
      <span class="menu-shortcut">Alt+Right</span>
    </li>
    <li class="menu-item">
      <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="23 4 23 10 17 10"></polyline>
        <polyline points="1 20 1 14 7 14"></polyline>
        <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
      </svg>
      <span>Reload</span>
      <span class="menu-shortcut">Ctrl+R</span>
    </li>
    <li class="menu-separator"></li>
    <li class="menu-item">
      <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
        <polyline points="17 21 17 13 7 13 7 21"></polyline>
        <polyline points="7 3 7 8 15 8"></polyline>
      </svg>
      <span>Save As...</span>
      <span class="menu-shortcut">Ctrl+S</span>
    </li>
    <li class="menu-item">
      <svg class="menu-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="6 9 6 2 18 2 18 9"></polyline>
        <path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
        <rect x="6" y="14" width="12" height="8"></rect>
      </svg>
      <span>Print...</span>
      <span class="menu-shortcut">Ctrl+P</span>
    </li>
  </ul>
</div>


.context-area {
  width: 100%;
  max-width: 600px;
  height: 400px;
  margin: 0 auto;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #ffffff;
  border: 2px dashed #cbd5e1;
  border-radius: 16px;
  color: #64748b;
  user-select: none;
  transition: border-color 0.3s ease;
}

.context-area:hover {
  border-color: #94a3b8;
}

.context-message {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.context-message p {
  margin: 0;
  font-weight: 500;
}

/* Context Menu */
.custom-context-menu {
  display: none;
  position: absolute;
  z-index: 9999;
  background: rgba(255, 255, 255, 0.85);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 12px;
  box-shadow: 
    0 4px 6px -1px rgba(0, 0, 0, 0.05),
    0 10px 15px -3px rgba(0, 0, 0, 0.1),
    0 20px 25px -5px rgba(0, 0, 0, 0.05);
  padding: 6px;
  min-width: 240px;
  color: #1e293b;
  
  /* Initial hidden state for animation */
  visibility: hidden;
  opacity: 0;
  transform: scale(0.95);
  transform-origin: top left;
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
}

.custom-context-menu.active {
  visibility: visible;
  opacity: 1;
  transform: scale(1);
}

.menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.menu-item {
  display: flex;
  align-items: center;
  padding: 10px 12px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.15s ease;
}

.menu-item:hover {
  background-color: #f1f5f9;
  color: #0f172a;
}

.menu-icon {
  width: 16px;
  height: 16px;
  margin-right: 12px;
  color: #64748b;
  transition: color 0.15s ease;
}

.menu-item:hover .menu-icon {
  color: #3b82f6;
}

.menu-shortcut {
  margin-left: auto;
  font-size: 12px;
  color: #94a3b8;
  font-weight: 400;
  letter-spacing: 0.5px;
}

.menu-separator {
  height: 1px;
  background-color: #e2e8f0;
  margin: 6px 4px;
}
  function initContextMenu() {
    const contextArea = document.querySelector('.context-area');
    const contextMenu = document.getElementById('context-menu');
    if (!contextArea || !contextMenu) return;
    let isMenuVisible = false;

    const showMenu = (x, y) => {
      contextMenu.style.visibility = 'hidden';
      contextMenu.style.display = 'block';
      
      const menuRect = contextMenu.getBoundingClientRect();
      const wrapperRect = contextArea.parentElement.getBoundingClientRect();
      
      let localX = x - wrapperRect.left;
      let localY = y - wrapperRect.top;
      
      let finalX = localX;
      let finalY = localY;

      if (finalX + menuRect.width > wrapperRect.width) {
        finalX = wrapperRect.width - menuRect.width - 8;
      }
      
      if (finalY + menuRect.height > wrapperRect.height) {
        finalY = wrapperRect.height - menuRect.height - 8;
      }

      const originX = finalX < localX ? 'right' : 'left';
      const originY = finalY < localY ? 'bottom' : 'top';
      contextMenu.style.transformOrigin = originX + ' ' + originY;

      contextMenu.style.left = finalX + 'px';
      contextMenu.style.top = finalY + 'px';
      
      contextMenu.style.visibility = 'visible';
      
      requestAnimationFrame(() => {
        contextMenu.classList.add('active');
      });
      
      isMenuVisible = true;
    };

    const hideMenu = () => {
      if (isMenuVisible) {
        contextMenu.classList.remove('active');
        setTimeout(() => {
          if (!contextMenu.classList.contains('active')) {
            contextMenu.style.display = 'none';
          }
        }, 150);
        isMenuVisible = false;
      }
    };

    contextArea.addEventListener('contextmenu', (e) => {
      e.preventDefault();
      if (isMenuVisible) {
        contextMenu.classList.remove('active');
        setTimeout(() => { showMenu(e.clientX, e.clientY); }, 50);
      } else {
        showMenu(e.clientX, e.clientY);
      }
    });

    document.addEventListener('click', (e) => {
      if (isMenuVisible && !contextMenu.contains(e.target)) {
        hideMenu();
      }
    });

    document.addEventListener('scroll', () => { hideMenu(); }, { passive: true });

    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') hideMenu();
    });
    
    const menuItems = document.querySelectorAll('.menu-item');
    menuItems.forEach(item => {
      item.addEventListener('click', () => { hideMenu(); });
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initContextMenu);
  } else {
    initContextMenu();
  }