Контекстное меню — это как тайный помощник веб-интерфейса: оно появляется в нужный момент и предлагает именно те действия, которые вам нужны. Правый клик мышью или долгое касание на экране смартфона — и вот перед вами список опций, готовых упростить жизнь. Но стандартные меню браузеров часто выглядят как гости из прошлого: серые, скучные и негибкие. Хотите, чтобы ваш сайт сиял? Пора создать кастомное контекстное меню! В этой статье мы разберём, что такое контекстное меню, почему его стоит настраивать и рассмотрим с глубоким погружением в логику кода пример реализации.

Что такое контекстное меню?

Контекстное меню — это всплывающая панель, которая появляется при определённых действиях, таких как правый клик (contextmenu) или долгое касание (touchstart). Оно предлагает действия, связанные с элементом, на который вы кликнули: от «Копирования текста» до «Добавления товара в корзину».

Особенность контекстного меню — его контекстная природа. Оно меняется в зависимости от того, где вы кликнули: на тексте, изображении или кнопке. Это делает его незаменимым для быстрого доступа к функциям без лишнего нагромождения интерфейса. Однако стандартные браузерные меню часто не вписываются в дизайн сайта и ограничивают функционал. Кастомное меню решает эту проблему, позволяя создать стильный, функциональный и брендированный элемент интерфейса.

Зачем кастомизировать контекстное меню?

Кастомизация контекстного меню — это шаг к созданию запоминающегося пользовательского опыта. Вот почему это важно:

  • Интуитивность: Кастомное меню гармонично вписывается в дизайн сайта, делая взаимодействие естественным и приятным.
  • Уникальные функции: Добавьте специфические действия, например, «Поделиться в Telegram» или «Сохранить в коллекцию», которых нет в стандартных меню.
  • Адаптивность: Поддержка мыши и сенсорных экранов обеспечивает единый опыт на всех устройствах.
  • Эстетика: С фирменными цветами, иконками и анимациями меню становится частью вашего бренда.
  • Конкурентное преимущество: Большинство сайтов используют стандартные решения. Кастомное меню выделит ваш проект.

Подумайте о вашем сайте как о театре: стандартное меню — это скучная афиша, а кастомное — это яркий занавес, который задаёт тон всему представлению.

Как создать кастомное контекстное меню: Пошаговое руководство

Мы создадим современное контекстное меню с плавными анимациями, иконками, поддержкой сенсорных устройств и доступностью. Используем HTML для структуры, CSS для стиля и JavaScript для интерактивности. Ниже — шаги и примеры кода, с особым вниманием к JavaScript.

Шаг 1: Создаём структуру с HTML

Меню — это div с атрибутами ARIA для доступности и идентификатором для его обработки.

<div class="context-menu" id="context-menu" role="menu" aria-hidden="true">
  <div class="context-menu__item" role="menuitem">
    Просмотр профиля
  </div>
  <div class="context-menu__item" role="menuitem">
    Обновить
  </div>
  <div class="context-menu__item" role="menuitem">
    Копировать ссылку
  </div>
</div>

Атрибуты role="menu" и role="menuitem" помогают экранным читалкам, а aria-hidden="true" скрывает меню, пока оно не открыто.

Для красоты в пункты меню можно добавить иконки и разделители для группировки опций.

Шаг 2: Стилизуем меню с CSS

С помощью стилей следует создать современный или брендовый вид меню. Обязательно следует выставить фиксированное позиционирование для того, чтобы отображать меню в месте его вызова.

.context-menu {
  position: fixed;
  background: #ffffff;
  border-radius: 8px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
  width: 180px;
  padding: 8px 0;
  visibility: hidden;
  opacity: 0;
}

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

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

Поддержка prefers-reduced-motion учитывает доступность а анимации делают появление меню плавным.

Шаг 3: Добавляем интерактивность с JavaScript

JavaScript — сердце нашего меню. Он управляет вызовом, позиционированием, закрытием и адаптацией под устройства. Код организован в класс ContextMenu для модульности и удобства поддержки.

class ContextMenu {
  
}

Давайте разберём подробно его начинку.

1. Конструктор класса

constructor(menuId) {
  this.menu = document.getElementById(menuId);
  this.isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
  this.lastTap = 0;
  this.timeout = null;
  this.init();
}

Назначение: Инициализирует класс, задавая начальные свойства.

  • this.menu: Сохраняет ссылку на элемент меню (<div id="context-menu">) для оптимизации (избегаем повторных DOM-запросов).
  • this.isTouchDevice: Проверяет, поддерживает ли устройство сенсорный ввод, используя ontouchstart in window (для современных браузеров) и navigator.maxTouchPoints (для устройств вроде Surface). Это позволяет адаптировать поведение для мыши или касаний.
  • this.lastTap и this.timeout: Переменные для отслеживания двойного касания на сенсорных устройствах.
  • this.init(): Вызывает метод инициализации сразу после создания экземпляра.

Почему это важно? Кэширование DOM-элемента и определение типа устройства с самого начала минимизируют вычисления и делают код готовым к разным сценариям.

2. Метод init

init() {
  const events = this.isTouchDevice ? ['touchstart'] : ['contextmenu'];
  
  events.forEach(eventType => {
    document.addEventListener(eventType, (e) => {
      e.preventDefault();
      this.showMenu(e);
    }, { passive: false });
  });

  if (this.isTouchDevice) {
    document.addEventListener('touchend', this.handleDoubleTap.bind(this), { passive: false });
  }

  document.addEventListener('click', (e) => {
    if (!this.menu.contains(e.target)) {
      this.hideMenu();
    }
  });

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
      this.hideMenu();
    }
  });
}

Назначение: Настраивает обработчики событий для вызова и закрытия меню.

Выбор событий:

  • Если устройство сенсорное (this.isTouchDevice), используется событие touchstart (начало касания).
  • Иначе — contextmenu (правый клик мыши).
  • Это делает меню универсальным для десктопов и мобильных устройств.

Обработчик вызова меню:

  • e.preventDefault() предотвращает стандартное поведение браузера (например, открытие браузерного контекстного меню при правом клике).
  • Вызывает this.showMenu(e) для отображения меню (описано ниже).
  • Опция { passive: false } позволяет использовать preventDefault в обработчике (по умолчанию браузеры могут игнорировать такие вызовы для оптимизации).

Двойное касание:

  • Для сенсорных устройств добавляется обработчик touchend (конец касания), который вызывает handleDoubleTap (описано ниже).
  • Метод bind(this) привязывает контекст this к методу, чтобы он мог обращаться к свойствам класса.

Закрытие меню:

  • Событие click проверяет, кликнул ли пользователь вне меню (!this.menu.contains(e.target)). Если да, меню скрывается методом hideMenu (описано ниже).
  • Событие keydown закрывает меню при нажатии клавиши Escape, улучшая доступность для клавиатурных пользователей.

Почему это важно? Разделение событий по типу устройства и добавление нескольких способов закрытия меню (клик, Escape) делают интерфейс гибким и интуитивным. Использование preventDefault гарантирует, что наше меню полностью заменяет стандартное.

3. Метод showMenu

showMenu(e) {
  const { clientX, clientY } = e.type === 'touchstart' ? e.touches[0] : e;
  const { width: menuWidth, height: menuHeight } = this.menu.getBoundingClientRect();
  const { innerWidth: winWidth, innerHeight: winHeight } = window;

  let posX = clientX;
  let posY = clientY;

  if (winWidth - clientX < menuWidth) {
    posX = winWidth - menuWidth - 5;
  }
  if (winHeight - clientY < menuHeight) {
    posY = winHeight - menuHeight - 5;
  }

  this.menu.style.left = `${posX}px`;
  this.menu.style.top = `${posY}px`;
  this.menu.classList.add('active');
  this.menu.setAttribute('aria-hidden', 'false');
}

Назначение: Отображает меню в правильной позиции на экране.

Координаты:

  • Извлекает координаты клика/касания (clientX, clientY).
  • Для сенсорных событий (touchstart) использует e.touches[0], так как касание содержит массив точек контакта.
  • Для мыши (contextmenu) берёт координаты напрямую из события.

Размеры:

  • getBoundingClientRect() возвращает размеры меню (menuWidth, menuHeight), чтобы проверить, помещается ли оно на экране.
  • window.innerWidth и window.innerHeight дают размеры видимой области окна.

Позиционирование:

  • По умолчанию меню появляется в точке клика (posX = clientX, posY = clientY).
  • Если меню выходит за правый край (winWidth - clientX < menuWidth), оно сдвигается влево (winWidth - menuWidth - 5), с отступом 5 пикселей для красоты.
  • Если выходит за нижний край (winHeight - clientY < menuHeight), сдвигается вверх (winHeight - menuHeight - 5).

Отображение:

  • Устанавливает CSS-свойства left и top для позиционирования.
  • Добавляет класс active, который активирует CSS-анимации (прозрачность и масштаб).
  • Меняет aria-hidden на false, чтобы экранные читалки видели меню.

Почему это важно? Умная логика позиционирования предотвращает обрезку меню за краями экрана, а ARIA-атрибуты обеспечивают доступность. Деструктуризация ({ clientX, clientY }) и лаконичные проверки делают код читаемым и эффективным.

4. Метод hideMenu

hideMenu() {
  this.menu.classList.remove('active');
  this.menu.setAttribute('aria-hidden', 'true');
}

Назначение: Скрывает меню.

  • Удаляет класс active, возвращая меню в скрытое состояние (CSS: visibility: hidden, opacity: 0).
  • Устанавливает aria-hidden="true", чтобы экранные читалки игнорировали меню.

Почему это важно? Простая функция повторно используется в разных сценариях (клик вне меню, Escape, двойное касание), поддерживая принцип DRY (Don’t Repeat Yourself).

5. Метод handleDoubleTap

handleDoubleTap(e) {
  const currentTime = new Date().getTime();
  const tapLength = currentTime - this.lastTap;

  clearTimeout(this.timeout);

  if (tapLength < 500 && tapLength > 0) {
    this.hideMenu();
    e.preventDefault();
  } else {
    this.timeout = setTimeout(() => {
      clearTimeout(this.timeout);
    }, 500);
  }

  this.lastTap = currentTime;
}

Назначение: Обрабатывает двойное касание на сенсорных устройствах для закрытия меню.

Логика:

  • currentTime фиксирует текущее время (в миллисекундах).
  • tapLength вычисляет разницу между текущим и предыдущим касанием (this.lastTap).
  • clearTimeout(this.timeout) очищает предыдущий таймер, чтобы избежать конфликтов.
  • Если tapLength меньше 500 мс и больше 0 (двойное касание), меню скрывается (hideMenu), и e.preventDefault() предотвращает другие действия (например, выделение текста).
  • Иначе запускается таймер на 500 мс, чтобы сбросить ожидание второго касания.
  • this.lastTap: Обновляется текущим временем для следующего касания.

Почему это важно? Двойное касание — интуитивный способ закрытия меню на сенсорных устройствах, аналогичный клику вне меню на десктопе. Логика таймера предотвращает случайные срабатывания.

6. Инициализация

class ContextMenu {
  // весь предыдущий код 
};

new ContextMenu('context-menu');
  • Создаёт экземпляр класса, передавая ID меню (context-menu), и запускает весь механизм.

Почему это важно? Объектно-ориентированный подход позволяет легко создавать несколько меню на странице, просто передав разные ID.

Готовый пример кастомного контекстного меню:

Заключение

Кастомное меню идеально для:

  • Веб-приложений: Быстрые команды в редакторах или дашбордах.
  • Социальных платформ: Опции для постов, например, «Лайк» или «Комментировать».
  • Электронной коммерции: Действия для товаров, такие как «В избранное».
  • Игр: Управление игровыми объектами прямо в интерфейсе.

Контекстное меню — это маленький элемент с большим потенциалом. Кастомизируя его, вы улучшаете пользовательский опыт, добавляете уникальности и показываете мастерство веб-разработки. С HTML, CSS и хорошо продуманным JavaScript вы можете создать меню, которое будет радовать глаз и душу. Экспериментируйте, добавляйте свой стиль и удивляйте пользователей!