Mega menu krok po kroku: HTML, CSS i JavaScript z dostępnością

Jak zrobić mega menu na stronie: kod HTML, CSS i JavaScript do skopiowania, obsługa klawiatury i czytników ekranu, wersja na telefon i najczęstsze błędy. Z działającym demo.

Ilustracja: pasek nawigacji strony z rozwiniętym panelem mega menu i kursorem na wyróżnionej pozycji
Ilustracja wygenerowana przez AI

Mega menu to duży panel, który rozwija się z górnego menu i pokazuje naraz kilka grup linków, często z krótkimi opisami. Znasz je ze sklepów internetowych i stron firm z rozbudowaną ofertą. Dobrze zrobione skraca klientowi drogę do konkretnej usługi. Źle zrobione zamyka się, zanim kursor do niego dojedzie, nie działa na telefonie i nie da się go obsłużyć klawiaturą.

W tym poradniku zbudujemy mega menu od zera: HTML, CSS i około 30 linii JavaScriptu, bez żadnej biblioteki. Kod sprawdziłem w przeglądarce, a działające demo otworzysz tutaj. Ten sam wzorzec działa w moim własnym menu: przycisk „Darmowe narzędzia” u góry tej strony.

Kiedy mega menu ma sens

Mega menu przydaje się, gdy:

  • w jednej części oferty masz więcej niż 7–8 podstron, na przykład kilka rodzajów usług w kilku grupach,
  • oferta naturalnie dzieli się na kategorie (sklep, firma budowlana, biuro rachunkowe z usługami dla różnych klientów),
  • chcesz pokazać przy linkach krótki opis albo jedno wyróżnione wezwanie do działania.

Jeśli cała strona ma 5–6 podstron, zwykłe menu wystarczy. Mega menu z trzema linkami wygląda pusto i tylko wydłuża kod.

Jak ma działać dobre mega menu

Zanim zaczniesz pisać kod, ustal zasady. Każda z nich ma swój powód:

  1. Otwiera się po kliknięciu, a z klawiatury klawiszem Enter lub spacją. Samo najechanie myszką nie działa na telefonie.
  2. Zamyka się po kliknięciu obok, klawiszem Escape i wtedy, gdy klawiszem Tab przejdziesz dalej.
  3. Czytnik ekranu wie, czy panel jest rozwinięty dzięki atrybutowi aria-expanded.
  4. Linki to zwykłe znaczniki <a> w HTML, więc Google widzi wszystkie podstrony.
  5. Między przyciskiem a panelem nie ma szczeliny, w której kursor „gubi” menu.
  6. Na telefonie panel zamienia się w listę rozwijaną pod przyciskiem „Menu”.
  7. Grupy mają nagłówki, a cały panel ma najwyżej jedno wyróżnione wezwanie do działania.
Mega menu na komputerze: trzy kolumny linków z krótkimi opisami i niebieska karta „Bezpłatny pomiar”
Demo na komputerze: trzy grupy linków z opisami i jedno wezwanie do działania.

Krok 1: HTML

Każda rozwijana pozycja to przycisk i panel. Przycisk, a nie link, bo niczego nie otwiera pod nowym adresem, tylko pokazuje panel. Atrybut aria-controls wskazuje panel, a hidden ukrywa go na starcie:

<header class="top">
  <div class="top__in">
    <a class="logo" href="/">Pomost</a>
    <button class="burger" type="button" aria-expanded="false" aria-controls="menu">Menu</button>

    <nav class="nav" id="menu" aria-label="Menu główne">
      <ul class="nav__list">
        <li>
          <button class="nav__toggle" type="button" aria-expanded="false" aria-controls="mm-oferta">Oferta</button>
          <div class="mega" id="mm-oferta" hidden>
            <div class="mega__in">
              <div>
                <p class="mega__title" id="mm-tarasy">Tarasy</p>
                <ul aria-labelledby="mm-tarasy">
                  <li><a href="/tarasy-kompozytowe">Tarasy kompozytowe<small>Bez malowania, na lata</small></a></li>
                  <li><a href="/tarasy-drewniane">Tarasy drewniane<small>Modrzew, sosna, egzotyki</small></a></li>
                </ul>
              </div>
              <!-- kolejne kolumny: Ogrody, Dodatki -->
              <div class="mega__promo">
                <b>Bezpłatny pomiar</b>
                <p>Przyjedziemy, zmierzymy i w 2 dni prześlemy wycenę.</p>
                <a href="/kontakt">Umów pomiar</a>
              </div>
            </div>
          </div>
        </li>
        <li><a href="/realizacje">Realizacje</a></li>
        <li><a href="/kontakt">Kontakt</a></li>
      </ul>
    </nav>
  </div>
</header>

Nagłówek grupy jest zwykłym akapitem, a lista łączy się z nim przez aria-labelledby. Czytnik ekranu ogłasza wtedy „Tarasy, lista, 3 pozycje”, a w strukturze nagłówków strony nie pojawia się kilkanaście dodatkowych H2.

Często spotykam w takich menu role="menu" i role="menuitem". To błąd: te role są przeznaczone dla menu aplikacji (jak „Plik, Edycja” w edytorze) i obiecują obsługę strzałkami, której zwykła strona nie ma. Do nawigacji wytyczne WAI-ARIA Authoring Practices polecają właśnie wzorzec „disclosure”, czyli przycisk, który pokazuje i ukrywa zawartość.

Krok 2: CSS

Najważniejsze są trzy rzeczy: panel przylega do nagłówka bez szczeliny, wygląd przycisku zależy od aria-expanded, a fokus z klawiatury jest dobrze widoczny:

.top { position: relative; background: #fff; }
.nav__list { display: flex; gap: 4px; margin: 0; padding: 0; list-style: none; }
.nav__toggle { display: inline-flex; align-items: center; gap: 8px; padding: 10px 14px;
  border: 0; border-radius: 10px; background: none; font: inherit; cursor: pointer; }

/* strzałka obraca się, gdy panel jest otwarty: stan czytany prosto z aria-expanded */
.nav__toggle::after { content: ""; width: 7px; height: 7px; border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor; transform: translateY(-2px) rotate(45deg); transition: transform .2s; }
.nav__toggle[aria-expanded="true"]::after { transform: translateY(1px) rotate(-135deg); }

/* panel na całą szerokość, zaczyna się dokładnie pod nagłówkiem (top: 100%, bez marginesu) */
.mega { position: absolute; left: 0; right: 0; top: 100%; z-index: 10; background: #fff;
  box-shadow: 0 24px 40px -24px rgba(17, 21, 28, .25); }
.mega__in { max-width: 1120px; margin: 0 auto; padding: 28px 20px 32px;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(0, 1.2fr); gap: 32px; }
.mega ul { margin: 0; padding: 0; list-style: none; }
.mega ul a { display: block; padding: 8px 10px; border-radius: 10px; text-decoration: none; }
.mega ul small { display: block; color: #5b6472; font-size: 13.5px; }

:focus-visible { outline: 3px solid #2f5bff; outline-offset: 2px; }

/* krótkie wejście panelu, ale nie dla osób, które wyłączyły animacje w systemie */
@media (prefers-reduced-motion: no-preference) {
  .mega:not([hidden]) { animation: wjazd .18s ease-out; }
}
@keyframes wjazd { from { opacity: 0; transform: translateY(-6px); } }

Zwróć uwagę na minmax(0, 1fr) w siatce. Bez tego długie słowo w kolumnie potrafi rozepchnąć panel szerzej niż ekran.

Krok 3: JavaScript

Skrypt robi cztery rzeczy: otwiera i zamyka panel po kliknięciu, zamyka inne panele, obsługuje Escape i zamyka menu, gdy klikniesz obok albo przejdziesz klawiszem Tab poza menu:

const nav = document.querySelector('.nav');
const toggles = [...nav.querySelectorAll('.nav__toggle')];

function setOpen(btn, open) {
  btn.setAttribute('aria-expanded', String(open));
  document.getElementById(btn.getAttribute('aria-controls')).hidden = !open;
}
function closeAll(except) {
  toggles.forEach((b) => { if (b !== except) setOpen(b, false); });
}

toggles.forEach((btn) => btn.addEventListener('click', () => {
  const open = btn.getAttribute('aria-expanded') !== 'true';
  closeAll(btn);
  setOpen(btn, open);
}));

// Escape zamyka otwarty panel i wraca do jego przycisku
nav.addEventListener('keydown', (e) => {
  const btn = toggles.find((b) => b.getAttribute('aria-expanded') === 'true');
  if (e.key === 'Escape' && btn) { setOpen(btn, false); btn.focus(); }
});

// kliknięcie poza menu i przejście fokusem na element poza menu zamykają panel
document.addEventListener('click', (e) => { if (!nav.contains(e.target)) closeAll(); });
document.addEventListener('focusin', (e) => { if (!nav.contains(e.target)) closeAll(); });

Stan menu jest zapisany w jednym miejscu: w atrybutach aria-expanded i hidden. CSS czyta go do obracania strzałki, czytnik ekranu do ogłaszania „rozwinięty” i „zwinięty”, a skrypt do decyzji, co zamknąć. Nie ma osobnych klas typu is-open, które mogłyby się rozjechać z tym, co słyszy osoba niewidoma.

Ciekawostka z testów: pierwsza wersja zamykała menu w zdarzeniu focusout. Działało z klawiatury, ale panel znikał po kliknięciu w jego tło, bo przeglądarka zdejmuje wtedy fokus z przycisku. Zdarzenie focusin na całym dokumencie reaguje tylko wtedy, gdy fokus faktycznie trafia na inny element, więc nie ma tego problemu.

Krok 4: wersja na telefon

Na telefonie nie ma miejsca na cztery kolumny. Ten sam HTML zamieniamy w listę rozwijaną: nawigacja chowa się za przyciskiem „Menu”, a panele układają się jeden pod drugim:

.burger { display: none; }
@media (max-width: 860px) {
  .burger { display: inline-flex; margin-left: auto; padding: 10px 14px;
    border: 1px solid #e3e6eb; border-radius: 10px; background: none; font: inherit; }
  .nav { display: none; position: absolute; left: 0; right: 0; top: 100%; z-index: 10;
    max-height: calc(100dvh - 68px); overflow-y: auto; background: #fff; }
  .nav.is-open { display: block; }
  .nav__list { flex-direction: column; padding: 8px 12px 16px; }
  .nav__toggle { width: 100%; justify-content: space-between; padding: 14px 12px; }
  .mega { position: static; box-shadow: none; }
  .mega__in { grid-template-columns: minmax(0, 1fr); gap: 20px; padding: 8px 12px 16px; }
}
const burger = document.querySelector('.burger');
burger.addEventListener('click', () => {
  const open = burger.getAttribute('aria-expanded') !== 'true';
  burger.setAttribute('aria-expanded', String(open));
  nav.classList.toggle('is-open', open);
});

max-height z jednostką dvh i overflow-y: auto sprawiają, że długie menu da się przewinąć na małym ekranie, a pasek adresu przeglądarki w telefonie go nie zasłania.

Mega menu na telefonie: przycisk Menu, rozwinięta pozycja Oferta i grupy linków jedna pod drugą
Na telefonie ten sam kod działa jak lista rozwijana pod przyciskiem „Menu”.

Jak przetestować menu w 5 minut

Zanim opublikujesz menu, sprawdź je tak, jak będą go używać ludzie:

  • Klawiatura: klawiszem Tab dojdź do „Oferta”, Enter otwiera panel, kolejne Taby przechodzą po linkach, Escape zamyka panel i wraca na przycisk. Tab za ostatnim linkiem zamyka panel.
  • Myszka: przejedź kursorem z przycisku do panelu ukosem i kliknij w puste miejsce panelu. Panel ma zostać otwarty.
  • Czytnik ekranu: w NVDA (Windows) albo VoiceOver (Mac, iPhone) przycisk powinien brzmieć „Oferta, przycisk, zwinięty”, a po otwarciu „rozwinięty”.
  • Telefon i powiększenie: przy szerokości 320 px i powiększeniu 200% nic nie może wychodzić poza ekran ani wymuszać przewijania w bok.
  • Lighthouse w Chrome (zakładka Dostępność) wyłapie brakujące nazwy przycisków i zbyt słaby kontrast.

Ogólny stan strony, w tym dostępność, SEO i szybkość, sprawdzisz w 30 sekund w moim narzędziu Zbadaj stronę.

Najczęstsze błędy w mega menu

  • Menu tylko na najechanie. Na telefonie nie da się go otworzyć, a na komputerze otwiera się przypadkiem.
  • Szczelina między przyciskiem a panelem. Wystarczy margin-top: 8px, żeby kursor w drodze do panelu wypadł z menu i wszystko się zamknęło.
  • <a href="#"> zamiast przycisku. Strona przeskakuje na górę, a czytnik ekranu ogłasza link, który nigdzie nie prowadzi.
  • role="menu" bez obsługi strzałek. Obietnica, której strona nie spełnia, jest gorsza niż brak ARIA.
  • Linki doczytywane skryptem dopiero po najechaniu. Google może ich nie zobaczyć.
  • Za dużo linków. Panel ze 120 pozycjami to katalog, nie nawigacja. Klient szuka 2–3 sekundy i jeśli nie znajdzie, wraca do Google.
  • Panel szerszy niż ekran na telefonie. Klasyczny objaw: strona da się przesunąć w bok o kilka pikseli.

A co z WordPressem?

W WordPressie mega menu zwykle robi motyw albo wtyczka. Zanim ją zostawisz, przejdź tę samą listę testów: wiele wtyczek otwiera menu tylko na najechanie, nie ustawia aria-expanded i dokłada kilkadziesiąt kilobajtów skryptów na każdej podstronie. Czasem prościej i szybciej jest dopisać własne menu na bazie kodu z tego poradnika.

Podsumowanie

Dobre mega menu to przycisk z aria-expanded, panel z hidden, zwykłe linki w HTML i trzy sposoby zamykania: kliknięcie obok, Escape i Tab. Na telefonie ten sam kod działa jako lista rozwijana. Pełny kod z tego artykułu jest w demo: otwórz je, sprawdź myszką i klawiaturą, a potem podejrzyj źródło strony.

Jeśli Twoja strona ma rozbudowaną ofertę, a menu nie nadąża, napisz do mnie. Przebuduję nawigację w obecnej stronie albo zaprojektuję nową stronę firmową. Odpowiadam w 24 godziny, a nowi klienci dostają 20% rabatu na pierwsze zlecenie.

Najczęstsze pytania

Usługi związane z tematem

Masz podobny projekt?

Opisz, czego potrzebujesz, a w 24 godziny odeślę wycenę. Nowi klienci dostają 20% rabatu na pierwsze zlecenie.

Wyceń projektUmów rozmowę (20 min)

Czytaj dalej

AI11 października 20269 min czytania

Bezpieczny chatbot AI na stronie firmy: 8 zasad z przykładami kodu

Jak zabezpieczyć chatbota AI na stronie: prompt injection, podrabiana historia rozmowy, bezpieczne wyświetlanie odpowiedzi, limity kosztów, prywatność i AI Act. Zasady z działającego wdrożenia, z kodem PHP i JavaScript.