Wprowadzenie
Animowane menu nawigacyjne to nowoczesny sposób prezentowania linków w interfejsie strony internetowej. Dzięki niemu użytkownicy zyskują przejrzysty i atrakcyjny wizualnie dostęp do podstron. Kod, który tu omawiamy, został stworzony tak, aby dodatkowo zapamiętywał aktywną pozycję, co znacznie poprawia doświadczenie odwiedzających. Po ponownym wejściu na stronę lub jej odświeżeniu użytkownik od razu widzi, który element był ostatnio wybrany.
Struktura HTML
Podstawą działania jest sekcja HTML zawierająca pasek nawigacji w formie listy. Każdy element listy <li> zawiera link <a> oraz ikonę <ion-icon>, pobieraną z biblioteki Ionicons. Taki układ pozwala w łatwy sposób dodawać, usuwać lub modyfikować pozycje menu. Aby zmienić ikonę, wystarczy w atrybucie name
podać inną nazwę dostępnej ikony.
Stylizacja CSS
Animowane menu nawigacyjne korzysta z arkusza stylów, który definiuje wygląd, animacje oraz rozmieszczenie elementów. W kodzie zastosowano zmienne CSS (:root), takie jak –circle czy –icon, co ułatwia personalizację kolorów. Zmienna –move określa czas trwania animacji przesuwania pływającego kółka. Dzięki temu zmiana wyglądu jest szybka i intuicyjna, bez konieczności przeszukiwania całego kodu.
Obsługa JavaScript
JavaScript odpowiada za logikę przesuwania pływającego wskaźnika oraz zapamiętywanie aktywnej pozycji. Funkcja moveTo() oblicza środek wybranego elementu i ustawia tam kółko. Funkcja resolveActiveIndex() sprawdza zapis w localStorage lub dopasowuje aktywny link do bieżącej strony. Dzięki temu animowane menu nawigacyjne zapamiętuje wybór użytkownika między wizytami.
Dostosowanie do własnych potrzeb
Ten kod można łatwo dopasować do dowolnego projektu. Chcąc zmienić kolory, wystarczy edytować zmienne w sekcji :root. Dodawanie nowych pozycji menu polega na skopiowaniu elementu <li> i podaniu nowego href oraz ikony. W razie potrzeby można też zmienić wysokość paska (–bar-height) czy wielkość kółka (–bubble).
Responsywność i optymalizacja
Dzięki ResizeObserver i obsłudze zdarzenia resize, ponadto animowane menu nawigacyjne zachowuje poprawne położenie wskaźnika, dlatego wygląda dobrze na różnych rozdzielczościach. To kluczowe w projektach responsywnych, które muszą działać zarówno na komputerach, jak i urządzeniach mobilnych. Zmiana wartości –move pozwala dostosować szybkość animacji do preferencji użytkownika.
Podsumowanie
Animowane menu nawigacyjne to estetyczne i funkcjonalne rozwiązanie, które ułatwia korzystanie z witryny. Łączy atrakcyjny wygląd z użytecznością, a dzięki prostym mechanizmom personalizacji każdy twórca może dostosować je do swojego stylu. Kod jest przejrzysty i modularny, co pozwala wprowadzać modyfikacje bez ryzyka uszkodzenia całości. Użycie localStorage sprawia, że interfejs jest bardziej spójny i przyjazny dla odwiedzających.
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script> <script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script> <style> :root{ /* To tło jest używane również do przerwy wokół kółka */ --page-bg: radial-gradient(1200px 600px at 10% -10%, #ffdf6e 0, #ffcc4d 40%, #f7b733 70%, #f2a531 100%); --bar-bg:#262626; /* kolor paska */ --bar-height:60px; /* wysokość paska */ --bar-shadow: 0 26px 50px rgba(0,0,0,.35); --slot:60px; /* szerokość slotu na ikonę+label */ --gap:0px; /* odstęp (przerwa) wokół kółka */ --bubble:50px; /* średnica kółka */ --lift:34px; /* ile kółko wystaje ponad pasek (od środka kółka) */ --move:360ms; /* czas animacji przesuwu */ --circle:#00ADD9; /* kolor kółka (jak na screenie) */ --icon:#c9d1e2; /* kolor ikon na pasku */ --label:#cfd7e7; /* kolor napisów na pasku */ --icon-in:#ffffff; /* kolor ikony wewnątrz kółka */ } /* Wrapper tylko do podglądu */ .stage{width:min(100%);display:flex;justify-content:center} /* Pasek nawigacji */ .nav{ position:relative;display:flex;align-items:center;justify-content:center;width:100%;max-width:680px;height:var(--bar-height); background:var(--bar-bg);border-radius:var(--bar-radius); } .nav ul{ display:flex; margin:0; padding:0; } .nav li{ list-style:none; width:var(--slot); height:72px; position:relative; width:75px; } .nav .text:hover{ color:#00ADD9 !important; } .nav a{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;text-decoration:none} .nav .icon{ position:absolute; inset:0; display:grid; place-items:center; font-size:20px; color:var(--icon) } /* Ukryj wszystkie napisy */ .nav .text { opacity: 0; font-size: 10px !important; transition: opacity 0.3s ease, transform 0.3s ease; } /* Pokaż tylko dla aktywnego elementu (tam gdzie jest kółko) */ .list.active .text { opacity: 1; } /* Płynne ukrywanie ikony na pasku dla aktywnego elementu */ .list.active .icon { opacity: 0; transition: opacity 0.3s ease; } /* Ikony nieaktywne są widoczne */ .nav .icon { opacity: 1; transition: opacity 0.3s ease; } /* Pływające kółko – osobny element NAD paskiem */ .indicator{position:absolute;inset:0;pointer-events:none;z-index:3} .circle{position:absolute;width:var(--bubble);height:var(--bubble);border-radius:50%;background:var(--circle);display:grid;place-items:center; /* Pozycja: X jest animowana, Y tak, by kółko wystawało ponad pasek */ transform: translateX(var(--x,0)) translateY(calc(-0% - var(--lift))); transition: transform var(--move) cubic-bezier(.2,.8,.2,1); border-style: solid; border-color:white; border-width: 4px; } .circle ion-icon{font-size:28px;color:var(--icon-in)} /* PRZERWA wokół kółka – pseudo-element z tłem strony nad paskiem */ .circle::before{content:"";position:absolute;inset:calc(-1 * var(--gap));border-radius:50%;background:var(--page-bg);z-index:-1} </style> </head> <body> <div class="stage"> <nav class="nav" aria-label="Animated navbar"> <ul> <li class="list active"> <a href="https://www.swinickiwsieci.pl/" aria-label="Home"> <span class="icon"><ion-icon name="home-outline"></ion-icon></span> <span class="text">Strona główna</span> </a> </li> <li class="list"> <a href="https://www.swinickiwsieci.pl/baza-wiedzy/" aria-label="Book"> <span class="icon"><ion-icon name="book-outline"></ion-icon></span> <span class="text">Baza wiedzy</span> </a> </li> <li class="list"> <a href="https://www.swinickiwsieci.pl/wtyczki/" aria-label="Settings"> <span class="icon"><ion-icon name="flash-outline"></ion-icon> </span> <span class="text">Wtyczki</span> </a> </li> <li class="list"> <a href="https://www.swinickiwsieci.pl/indywidualna-wycena-strony/" aria-label="Alerts"> <span class="icon"><ion-icon name="notifications-outline"></ion-icon></span> <span class="text">Współpraca</span> </a> </li> <li class="list"> <a href="https://www.swinickiwsieci.pl/moje-konto/" aria-label="Profile"> <span class="icon"><ion-icon name="person-outline"></ion-icon></span> <span class="text">Zaloguj się</span> </a></li> </ul> <!-- Kółko z ikoną jako odrębny element --> <div class="indicator" aria-hidden="true"> <div class="circle" id="circle"> <ion-icon id="circleIcon" name="home-outline"></ion-icon> </div> </div> </nav> </div> <script> const items = Array.from(document.querySelectorAll('.list')); const links = Array.from(document.querySelectorAll('.list > a')); const circle = document.getElementById('circle'); const circleIcon = document.getElementById('circleIcon'); const nav = document.querySelector('.nav'); // klucz w storage const STORAGE_KEY = 'activeLinkPath'; // Ustal index aktywnego: preferuj pathname (stabilne między domeną/http/https) function resolveActiveIndex() { const saved = localStorage.getItem(STORAGE_KEY); // 1) najpierw spróbuj z localStorage if (saved) { const i = links.findIndex(a => new URL(a.href).pathname === saved); if (i >= 0) return i; } // 2) potem dopasuj do bieżącej strony const here = location.pathname; const j = links.findIndex(a => new URL(a.href).pathname === here); if (j >= 0) return j; // 3) fallback: pierwszy return 0; } function moveTo(index){ const wrap = nav.getBoundingClientRect(); const box = items[index].getBoundingClientRect(); const centerX = (box.left - wrap.left) + box.width/2; circle.style.setProperty('--x', `calc(${centerX}px - var(--bubble)/2)`); const icon = items[index].querySelector('ion-icon'); if(icon) circleIcon.setAttribute('name', icon.getAttribute('name')); items.forEach(el=>el.classList.remove('active')); items[index].classList.add('active'); } // Inicjalizacja dopiero gdy layout gotowy: // 1) po DOMContentLoaded // 2) po pełnym załadowaniu (obrazy, czcionki, ionicons) // 3) po ewentualnym reflow/resize let current = resolveActiveIndex(); function initPosition() { // 2x rAF – gwarantuje, że przeliczymy po finalnym reflow requestAnimationFrame(() => { requestAnimationFrame(() => moveTo(current)); }); } document.addEventListener('DOMContentLoaded', initPosition); window.addEventListener('load', initPosition); window.addEventListener('resize', () => moveTo(current)); // Dodatkowo pilnuj zmian rozmiaru samej nawigacji (np. gdy pokaże się scrollbar) if ('ResizeObserver' in window) { new ResizeObserver(() => moveTo(current)).observe(nav); } // Klik – zapisz pathname, animuj, potem przejdź links.forEach((a,i)=>{ a.addEventListener('click', (e)=>{ const href = a.getAttribute('href') || '#'; const isHash = href.startsWith('#'); current = i; localStorage.setItem(STORAGE_KEY, new URL(a.href).pathname); moveTo(i); e.preventDefault(); const delay = 360; // ms = var(--move) setTimeout(()=>{ if(isHash){ location.hash = href; } else { window.location.href = a.href; } }, delay); }); }); </script>