
В этом уроке Блейк Лундквист (Blake Lundquist) проведет нас через два метода создания навигационной панели с "подвижным выделением", используя только обычный JavaScript и CSS. Первый метод использует метод getBoundingClientRect
, чтобы явно анимировать границу между элементами навигационной панели при их нажатии. Второй подход достигает той же функциональности с помощью нового API View Transition.
Недавно я наткнулся на старый учебник по jQuery, демонстрирующий навигационную панель с "подвижным выделением", и решил, что эта концепция заслуживает современного обновления. В соответствии с данным шаблоном граница вокруг активного элемента навигации анимируется непосредственно от одного элемента к другому, когда пользователь нажимает на элементы меню. В 2025 году у нас есть гораздо лучшие инструменты для манипулирования DOM с помощью обычного JavaScript. Новые функции, такие как API View Transition, делают прогрессивное улучшение более легко достижимым и обрабатывают множество деталей анимации.

В этом уроке я продемонстрирую два метода создания навигационной панели с "подвижным выделением" с использованием обычного JavaScript и CSS. Первый пример использует метод getBoundingClientRect
, чтобы явно анимировать границу между элементами навигационной панели при их нажатии. Второй пример достигает той же функциональности с помощью нового API View Transition.
Начальная разметка
Давайте предположим, что у нас есть одностраничное приложение, где контент изменяется без перезагрузки страницы. Начальный HTML и CSS — это стандартная навигационная панель с дополнительным элементом div
с идентификатором #highlight
. У первого навигационного элемента ставим класс .active
.
<nav>
<div id="highlight"></div>
<a href="#" class="active">Home</a>
<a href="#services">Services</a>
<a href="#about">About</a>
<a href="#contact">Contact</a>
</nav>
Для этой версии, чтобы создать рамку, мы будем позиционировать элемент #highlight
вокруг элемента с классом .active
. Мы можем использовать абсолютное позиционирование и анимировать элемент по навигационной панели, чтобы создать желаемый эффект. Мы спрячем его за экраном, добавив значение left: -200px
, и добавив стили перехода transition
для всех свойств, чтобы любые изменения позиции и размера элемента происходили постепенно.
#highlight {
z-index: 0;
position: absolute;
height: 100%;
width: 100px;
left: -200px;
border: 2px solid green;
box-sizing: border-box;
transition: all 0.2s ease;
}
Добавление обработчика событий для взаимодействия с кликами
Нам нужно, чтобы элемент выделения анимировался, когда пользователь меняет активный элемент навигации. Давайте добавим обработчик событий клика к элементу nav
, затем отфильтруем события, вызванные только элементами, соответствующими нашему желаемому селектору. В данном случае мы хотим изменить активный элемент навигации только в том случае, если пользователь нажимает на ссылку, у которой еще нет класса .active
.
Для отладки сначала будем вызываль запись в консоль console.log
, чтобы убедиться, что обработчик срабатывает только тогда, когда ожидается:
const navbar = document.querySelector('nav');
navbar.addEventListener('click', function (event) {
// выходим, если у нажатого элемента нет правильного селектора
if (!event.target.matches('nav a:not(active)')) {
return;
}
console.log('click');
});
Откройте консоль браузера и попробуйте нажимать на разные элементы в навигационной панели. Вы должны видеть слово "click", появляющееся только тогда, когда вы выбираете новый элемент в навигационной панели.
Теперь, когда мы знаем, что наш обработчик событий работает на правильных элементах, давайте добавим код для перемещения класса .active
на элемент навигации, который был нажат. Мы можем использовать объект, переданный в обработчик событий, чтобы найти элемент, который инициировал событие, и присвоить этому элементу класс .active
после удаления его с предыдущего активного элемента.
const navbar = document.querySelector('nav');
navbar.addEventListener('click', function (event) {
// выходим, если у нажатого элемента нет правильного селектора
if (!event.target.matches('nav a:not(active)')) {
return;
}
document.querySelector('nav a.active').classList.remove('active');
event.target.classList.add('active');
});
Наш элемент #highlight
должен перемещаться по навигационной панели и позиционироваться вокруг активного элемента. Давайте напишем функцию для расчета новой позиции и ширины. Поскольку селектор #highlight
имеет стили перехода, он будет перемещаться постепенно, когда его положение изменится.
Используя getBoundingClientRect
, мы можем получить информацию о положении и размере элемента. Мы рассчитываем ширину активного элемента навигации и его смещение от левой границы родительского элемента. Затем мы присваиваем стили элементу выделения, чтобы его размер и положение соответствовали.
// обработчик для передвижения выделения
const moveHighlight = () => {
const activeNavItem = document.querySelector('a.active');
const highlighterElement = document.querySelector('#highlight');
const width = activeNavItem.offsetWidth;
const itemPos = activeNavItem.getBoundingClientRect();
const navbarPos = navbar.getBoundingClientRect();
const relativePosX = itemPos.left - navbarPos.left;
const styles = {
left: `${relativePosX}px`,
width: `${width}px`,
};
Object.assign(highlighterElement.style, styles);
}
Давайте вызовем нашу новую функцию, когда срабатывает событие клика:
navbar.addEventListener('click', function (event) {
// выходим, если у нажатого элемента нет правильного селектора
if (!event.target.matches('nav a:not(active)')) {
return;
}
document.querySelector('nav a.active').classList.remove('active');
event.target.classList.add('active');
moveHighlight();
});
Наконец, давайте также вызовем функцию сразу с начала работы программы, чтобы рамка переместилась на наш начальный активный элемент, когда страница впервые загружается:
// обработчик для передвижения выделения
const moveHighlight = () => {
// ...
}
// отображаем выделение, когда страница загружается
moveHighlight();
Теперь рамка перемещается по навигационной панели, когда выбирается новый элемент. Попробуйте нажимать на разные ссылки навигации, чтобы анимировать навигационную панель.
Этот метод требует всего нескольких строк обычного JavaScript и может быть легко расширен для учета других взаимодействий, таких как событие mouseover
. В следующем разделе мы рассмотрим рефакторинг этой функции с использованием API View Transition.
Использование API View Transition
API View Transition предоставляет функциональность для создания анимированных переходов между представлениями веб-сайта. Под капотом API создает снимки "до" и "после" представлений, а затем обрабатывает переход между ними. Переходы представлений полезны для создания анимаций между документами, предоставляя опыт, похожий на нативные приложения, который характерен для таких фреймворков, как Astro. Однако API также предоставляет обработчики, предназначенные для приложений в стиле SPA. Мы будем использовать его, чтобы уменьшить объем JavaScript, необходимого в нашей реализации, и более легко создавать резервную функциональность.
Для этого подхода нам больше не нужен отдельный элемент #highlight
. Вместо этого мы можем напрямую стилизовать активный элемент навигации с помощью псевдо-селекторов и позволить API View Transition обрабатывать анимацию между состояниями пользовательского интерфейса "до" и "после", когда нажимается новый элемент навигации.
Начнем с удаления элемента #highlight
и связанного с ним CSS и замены его стилями для псевдо-селектора nav a::after
:
<nav>
<!-- <div id="highlight"></div> -->
<a href="#" class="active">Главная</a>
<a href="#services">Услуги</a>
<a href="#about">О нас</a>
<a href="#contact">Контакты</a>
</nav>
/* #highlight {
z-index: 0;
position: absolute;
height: 100%;
width: 0;
left: 0;
box-sizing: border-box;
transition: all 0.2s ease;
} */
nav a::after {
content: " ";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: none;
box-sizing: border-box;
}
Для класса .active
мы добавляем свойство view-transition-name
, тем самым открывая магию API View Transition. Как только мы запускаем переход представлений и меняем расположение элемента .active
в DOM, будут сделаны снимки "до" и "после", и браузер анимирует рамку по панели. Мы дадим нашему переходу представления имя highlight
, но теоретически мы могли бы дать ему любое имя.
nav a.active::after {
border: 2px solid green;
view-transition-name: highlight;
}
Когда у нас есть селектор, содержащий свойство view-transition-name
, остается только запустить переход, используя метод startViewTransition
и передав ему функцию обратного вызова.
const navbar = document.querySelector('nav');
// Меняем активный элемент навигации по нажатию
navbar.addEventListener('click', async function (event) {
if (!event.target.matches('nav a:not(.active)')) {
return;
}
document.startViewTransition(() => {
document.querySelector('nav a.active').classList.remove('active');
event.target.classList.add('active');
});
});
Приведенный выше код — это пересмотренная версия обработчика кликов. Вместо того чтобы делать все расчеты для размера и положения движущейся границы самостоятельно, API View Transition делает все это за нас. Нам нужно только вызвать document.startViewTransition
и передать ему функцию обратного вызова, чтобы изменить элемент, к которому применяется класс .active
!
Настройка перехода представления
На этом этапе, при нажатии на ссылку навигации, вы заметите, что переход работает, но видны некоторые странные проблемы с размером.

Эта несогласованность размеров вызвана изменением соотношения сторон в ходе перехода представления. Здесь мы не будем вдаваться в подробности этой проблемы. Но если говорить коротко то, чтобы обеспечить постоянную высоту рамки на протяжении всего перехода, нам нужно явно задать высоту для псевдо-селекторов ::view-transition-old
и ::view-transition-new
, представляющих статический снимок старого и нового вида соответственно.
::view-transition-old(highlight) {
height: 100%;
}
::view-transition-new(highlight) {
height: 100%;
}
Давайте проведем финальный рефакторинг, чтобы привести наш код в порядок, переместив обратный вызов в отдельную функцию и добавив резервный вариант для случаев, когда переходы представлений не поддерживаются:
const navbar = document.querySelector('nav');
// изменяем элемент, который получил класс .active
const setActiveElement = (elem) => {
document.querySelector('nav a.active').classList.remove('active');
elem.classList.add('active');
}
// Начинаем переход представления и передаем функцию обратного вызова при нажатии
navbar.addEventListener('click', async function (event) {
if (!event.target.matches('nav a:not(.active)')) {
return;
}
// Обратная совместимость для браузеров не поддерживающих API View Transitions:
if (!document.startViewTransition) {
setActiveElement(event.target);
return;
}
document.startViewTransition(() => setActiveElement(event.target));
});
Вот наша навигационная панель, управляемая переходом представления! Обратите внимание на плавный переход, когда вы нажимаете на разные ссылки:
Заключение
Анимации и переходы между состояниями пользовательского интерфейса веб-сайта раньше требовали многих килобайт внешних библиотек, наряду с многословным, запутанным и подверженным ошибкам кодом, но обычный JavaScript и CSS с тех пор включили функции, позволяющие достичь взаимодействия, подобного нативным приложениям, без ущерба для бюджета. Мы продемонстрировали это, реализовав навигационную панель с "подвижным выделением" с использованием двух подходов: CSS-переходов в сочетании с методом getBoundingClientRect()
и API View Transition.