← Назад к вопросам

Создать адаптивное навигационное меню

2.0 Middle🔥 171 комментариев
#HTML и CSS

Условие

Создайте адаптивное навигационное меню с мобильной версией (бургер-меню).

Требования

  1. Desktop версия (более 768px):

    • Горизонтальное меню со ссылками
    • Hover-эффекты на пунктах меню
    • Выделение активного пункта
  2. Mobile версия (менее 768px):

    • Кнопка-бургер для открытия меню
    • Выезжающее боковое меню или dropdown
    • Анимация открытия/закрытия
  3. Доступность:

    • Корректная работа с клавиатурой
    • ARIA-атрибуты для screen readers

Технологии

Можно использовать чистый CSS/JS или React.

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Задача на адаптивное навигационное меню — показывает понимание responsive design, accessibility и работы с состоянием. Создадим несколько версий.

Решение 1: React с Tailwind CSS

import React, { useState } from "react";

interface NavLink {
  label: string;
  href: string;
  icon?: string;
}

interface NavbarProps {
  brand: string;
  links: NavLink[];
  currentPath?: string;
}

function Navbar({ brand, links, currentPath = "/" }: NavbarProps) {
  const [isOpen, setIsOpen] = useState(false);

  const toggleMenu = () => {
    setIsOpen(!isOpen);
  };

  const closeMenu = () => {
    setIsOpen(false);
  };

  return (
    <nav
      className="bg-white shadow-lg"
      role="navigation"
      aria-label="Main navigation"
    >
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between items-center h-16">
          {/* Логотип/Бренд */}
          <div className="flex-shrink-0">
            <a
              href="/"
              className="text-2xl font-bold text-blue-600 hover:text-blue-700"
            >
              {brand}
            </a>
          </div>

          {/* Desktop Menu */}
          <div className="hidden md:flex space-x-1">
            {links.map((link) => (
              <a
                key={link.href}
                href={link.href}
                className={`px-4 py-2 rounded-lg font-medium transition-colors ${
                  currentPath === link.href
                    ? "bg-blue-600 text-white"
                    : "text-gray-700 hover:bg-gray-100"
                }`}
                aria-current={currentPath === link.href ? "page" : undefined}
              >
                {link.label}
              </a>
            ))}
          </div>

          {/* Mobile Menu Button */}
          <div className="md:hidden">
            <button
              onClick={toggleMenu}
              className="inline-flex items-center justify-center p-2 rounded-md text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-600"
              aria-expanded={isOpen}
              aria-controls="mobile-menu"
              aria-label="Toggle navigation menu"
            >
              {/* Hamburger Icon */}
              <svg
                className={`h-6 w-6 transition-transform ${
                  isOpen ? "transform rotate-90" : ""
                }`}
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M4 6h16M4 12h16M4 18h16"
                />
              </svg>
            </button>
          </div>
        </div>
      </div>

      {/* Mobile Menu */}
      {isOpen && (
        <div
          id="mobile-menu"
          className="md:hidden bg-gray-50 border-t"
          role="menu"
        >
          <div className="px-2 pt-2 pb-3 space-y-1">
            {links.map((link) => (
              <a
                key={link.href}
                href={link.href}
                onClick={closeMenu}
                className={`block px-3 py-2 rounded-lg font-medium transition-colors ${
                  currentPath === link.href
                    ? "bg-blue-600 text-white"
                    : "text-gray-700 hover:bg-gray-200"
                }`}
                role="menuitem"
                aria-current={currentPath === link.href ? "page" : undefined}
              >
                {link.label}
              </a>
            ))}
          </div>
        </div>
      )}
    </nav>
  );
}

export default Navbar;

Решение 2: С анимацией и закрытием клавишей Escape

function NavbarAdvanced({ brand, links, currentPath = "/" }: NavbarProps) {
  const [isOpen, setIsOpen] = useState(false);

  // Закрытие меню при нажатии Escape
  React.useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        setIsOpen(false);
      }
    };

    if (isOpen) {
      document.addEventListener("keydown", handleEscape);
      // Блокируем скроллинг при открытом меню
      document.body.style.overflow = "hidden";

      return () => {
        document.removeEventListener("keydown", handleEscape);
        document.body.style.overflow = "unset";
      };
    }
  }, [isOpen]);

  return (
    <nav className="bg-white shadow-lg" role="navigation">
      <div className="max-w-7xl mx-auto px-4">
        <div className="flex justify-between items-center h-16">
          {/* Логотип */}
          <a href="/" className="text-2xl font-bold text-blue-600">
            {brand}
          </a>

          {/* Desktop Menu */}
          <div className="hidden md:flex space-x-1">
            {links.map((link) => (
              <a
                key={link.href}
                href={link.href}
                className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 ${
                  currentPath === link.href
                    ? "bg-blue-600 text-white shadow-md"
                    : "text-gray-700 hover:bg-gray-100 hover:translate-y-[-2px]"
                }`}
              >
                {link.label}
              </a>
            ))}
          </div>

          {/* Mobile Burger Button */}
          <button
            onClick={() => setIsOpen(!isOpen)}
            className="md:hidden flex flex-col gap-1.5 p-2 hover:bg-gray-100 rounded-lg transition-colors"
            aria-expanded={isOpen}
            aria-label="Toggle navigation menu"
          >
            {/* Animated Hamburger */}
            <span
              className={`w-6 h-0.5 bg-gray-700 transition-all duration-300 ${
                isOpen ? "rotate-45 translate-y-2" : ""
              }`}
            />
            <span
              className={`w-6 h-0.5 bg-gray-700 transition-all duration-300 ${
                isOpen ? "opacity-0" : ""
              }`}
            />
            <span
              className={`w-6 h-0.5 bg-gray-700 transition-all duration-300 ${
                isOpen ? "-rotate-45 -translate-y-2" : ""
              }`}
            />
          </button>
        </div>
      </div>

      {/* Mobile Menu with Animation */}
      <div
        className={`md:hidden overflow-hidden transition-all duration-300 ${
          isOpen ? "max-h-64" : "max-h-0"
        }`}
      >
        <div className="bg-gray-50 px-4 py-3 space-y-2 border-t">
          {links.map((link) => (
            <a
              key={link.href}
              href={link.href}
              onClick={() => setIsOpen(false)}
              className={`block px-3 py-2 rounded-lg font-medium transition-colors ${
                currentPath === link.href
                  ? "bg-blue-600 text-white"
                  : "text-gray-700 hover:bg-gray-200"
              }`}
            >
              {link.label}
            </a>
          ))}
        </div>
      </div>

      {/* Overlay for Mobile */}
      {isOpen && (
        <div
          className="md:hidden fixed inset-0 bg-black bg-opacity-50 z-40"
          onClick={() => setIsOpen(false)}
          aria-hidden="true"
        />
      )}
    </nav>
  );
}

Решение 3: Боковое меню с анимацией

function NavbarSidebar({ brand, links, currentPath = "/" }: NavbarProps) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <nav className="bg-white shadow-lg" role="navigation">
        <div className="max-w-7xl mx-auto px-4">
          <div className="flex justify-between items-center h-16">
            <a href="/" className="text-2xl font-bold text-blue-600">
              {brand}
            </a>

            {/* Desktop Menu */}
            <div className="hidden md:flex space-x-1">
              {links.map((link) => (
                <a
                  key={link.href}
                  href={link.href}
                  className={`px-4 py-2 rounded-lg font-medium ${
                    currentPath === link.href
                      ? "bg-blue-600 text-white"
                      : "text-gray-700 hover:bg-gray-100"
                  }`}
                >
                  {link.label}
                </a>
              ))}
            </div>

            {/* Mobile Button */}
            <button
              onClick={() => setIsOpen(!isOpen)}
              className="md:hidden p-2 hover:bg-gray-100 rounded-lg"
              aria-expanded={isOpen}
              aria-label="Toggle navigation menu"
            >
              <svg className="w-6 h-6" fill="none" stroke="currentColor">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
              </svg>
            </button>
          </div>
        </div>
      </nav>

      {/* Sidebar Menu */}
      <div
        className={`fixed top-0 left-0 h-screen w-64 bg-white shadow-xl transform transition-transform duration-300 z-50 ${
          isOpen ? "translate-x-0" : "-translate-x-full"
        }`}
        role="menu"
      >
        <div className="p-4 border-b">
          <button
            onClick={() => setIsOpen(false)}
            className="p-2 hover:bg-gray-100 rounded-lg"
            aria-label="Close navigation menu"
          >
            <svg className="w-6 h-6" fill="none" stroke="currentColor">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
            </svg>
          </button>
        </div>
        <div className="p-4 space-y-2">
          {links.map((link) => (
            <a
              key={link.href}
              href={link.href}
              onClick={() => setIsOpen(false)}
              className={`block px-4 py-3 rounded-lg font-medium transition-colors ${
                currentPath === link.href
                  ? "bg-blue-600 text-white"
                  : "text-gray-700 hover:bg-gray-100"
              }`}
              role="menuitem"
            >
              {link.label}
            </a>
          ))}
        </div>
      </div>

      {/* Overlay */}
      {isOpen && (
        <div
          className="fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden"
          onClick={() => setIsOpen(false)}
          aria-hidden="true"
        />
      )}
    </>
  );
}

Пример использования

const navLinks: NavLink[] = [
  { label: "Home", href: "/" },
  { label: "About", href: "/about" },
  { label: "Services", href: "/services" },
  { label: "Blog", href: "/blog" },
  { label: "Contact", href: "/contact" },
];

<Navbar
  brand="MyApp"
  links={navLinks}
  currentPath={location.pathname}
/>

Важные моменты доступности

  1. ARIA-атрибуты:

    • role="navigation" — семантическая роль
    • aria-label — описание кнопок
    • aria-expanded — состояние меню
    • aria-current="page" — активный пункт
  2. Клавиатура:

    • Поддержка Escape для закрытия
    • Tab для навигации
    • Enter/Space для активации
  3. Screen Readers:

    • Правильная семантика
    • Описание интерактивных элементов
    • aria-hidden для декоративных элементов

Best Practices

  1. Responsive: Использовать Tailwind hidden md:flex
  2. Анимация: CSS transitions для плавного перехода
  3. State Management: useState для открытия/закрытия
  4. Effects: useEffect для Escape и блокировки скроллинга
  5. Доступность: ARIA-атрибуты и клавиатурная навигация

Рекомендации для собеседования

  1. Покажите базовую версию с Tailwind
  2. Добавьте анимацию бургер-кнопки
  3. Реализуйте закрытие по Escape
  4. Обсудите accessibility (ARIA, семантика)
  5. Покажите боковое меню вариант (красиво выглядит)

Лучший выбор для production: Вариант 2 с анимацией и управлением эффектами — полный функционал + доступность + UX.