{"id":941,"date":"2025-06-11T13:00:00","date_gmt":"2025-06-11T13:00:00","guid":{"rendered":"http:\/\/www.computercoursesonline.com\/?p=941"},"modified":"2025-06-12T21:04:55","modified_gmt":"2025-06-12T21:04:55","slug":"creating-the-moving-highlight-navigation-bar-with-javascript-and-css","status":"publish","type":"post","link":"http:\/\/www.computercoursesonline.com\/index.php\/2025\/06\/11\/creating-the-moving-highlight-navigation-bar-with-javascript-and-css\/","title":{"rendered":"Creating The “Moving Highlight” Navigation Bar With JavaScript And CSS"},"content":{"rendered":"

Creating The &ldquo;Moving Highlight&rdquo; Navigation Bar With JavaScript And CSS<\/title><\/p>\n<article>\n<header>\n<h1>Creating The &ldquo;Moving Highlight&rdquo; Navigation Bar With JavaScript And CSS<\/h1>\n<address>Blake Lundquist<\/address>\n<p> 2025-06-11T13:00:00+00:00<br \/>\n 2025-06-12T20:33:08+00:00<br \/>\n <\/header>\n<p>I recently came across an old jQuery tutorial demonstrating a <strong>\u201cmoving highlight\u201d navigation bar<\/strong> and decided the concept was due for a modern upgrade. With this pattern, the border around the active navigation item animates directly from one element to another as the user clicks on menu items. In 2025, we have much better tools to manipulate the DOM via vanilla JavaScript. New features like the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/View_Transition_API\">View Transition API<\/a> make progressive enhancement more easily achievable and handle a lot of the animation minutiae.<\/p>\n<figure><a href=\"http:\/\/www.computercoursesonline.com\/wp-content\/uploads\/2025\/06\/1-moving-highlight-navigation-bar.gif\"><img loading=\"lazy\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" width=\"800\" height=\"131\" alt=\"An example of a \u201cmoving highlight\u201d navigation bar\" class=\"lazyload\" data-src=\"http:\/\/www.computercoursesonline.com\/wp-content\/uploads\/2025\/06\/1-moving-highlight-navigation-bar.gif\"><\/a><figcaption>(<a href=\"http:\/\/www.computercoursesonline.com\/wp-content\/uploads\/2025\/06\/1-moving-highlight-navigation-bar.gif\">Large preview<\/a>)<\/figcaption><\/figure>\n<p>In this tutorial, I will demonstrate two methods of creating the \u201cmoving highlight\u201d navigation bar using plain JavaScript and CSS. The first example uses the <code>getBoundingClientRect<\/code> method to explicitly animate the border between navigation bar items when they are clicked. The second example achieves the same functionality using the new View Transition API.<\/p>\n<h2 id=\"the-initial-markup\">The Initial Markup<\/h2>\n<p>Let\u2019s assume that we have a single-page application where content changes without the page being reloaded. The starting HTML and CSS are your standard navigation bar with an additional <code>div<\/code> element containing an <code>id<\/code> of <code>#highlight<\/code>. We give the first navigation item a class of <code>.active<\/code>.<\/p>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"EajQyBW\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Moving Highlight Navbar Starting Markup [forked]](https:\/\/codepen.io\/smashingmag\/pen\/EajQyBW) by <a href=\"https:\/\/codepen.io\/blakeeric\">Blake Lundquist<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/EajQyBW\">Moving Highlight Navbar Starting Markup [forked]<\/a> by <a href=\"https:\/\/codepen.io\/blakeeric\">Blake Lundquist<\/a>.<\/figcaption><\/figure>\n<p>For this version, we will position the <code>#highlight<\/code> element around the element with the <code>.active<\/code> class to create a border. We can utilize <code>absolute<\/code> positioning and animate the element across the navigation bar to create the desired effect. We\u2019ll hide it off-screen initially by adding <code>left: -200px<\/code> and include <code>transition<\/code> styles for all properties so that any changes in the position and size of the element will happen gradually.<\/p>\n<pre><code class=\"language-css\">#highlight {\n z-index: 0;\n position: absolute;\n height: 100%;\n width: 100px;\n left: -200px;\n border: 2px solid green;\n box-sizing: border-box;\n transition: all 0.2s ease;\n}\n<\/code><\/pre>\n<div data-audience=\"non-subscriber\" data-remove=\"true\" class=\"feature-panel-container\">\n<aside class=\"feature-panel\">\n<div class=\"feature-panel-left-col\">\n<div class=\"feature-panel-description\">\n<p>Meet <strong><a data-instant href=\"\/printed-books\/touch-design-for-mobile-interfaces\/\">Touch Design for Mobile Interfaces<\/a><\/strong>, Steven Hoober\u2019s brand-new guide on <strong>designing for mobile<\/strong> with proven, universal, human-centric guidelines. <strong>400 pages<\/strong>, jam-packed with in-depth user research and <strong>best practices<\/strong>.<\/p>\n<p><a data-instant href=\"https:\/\/www.smashingmagazine.com\/printed-books\/touch-design-for-mobile-interfaces\/\" class=\"btn btn--green btn--large\">Jump to table of contents \u21ac<\/a><\/div>\n<\/div>\n<div class=\"feature-panel-right-col\"><a data-instant href=\"https:\/\/www.smashingmagazine.com\/printed-books\/touch-design-for-mobile-interfaces\/\" class=\"feature-panel-image-link\"><\/p>\n<div class=\"feature-panel-image\">\n<img loading=\"lazy\" class=\"feature-panel-image-img lazyload\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Feature Panel\" width=\"480\" height=\"697\" data-src=\"https:\/\/archive.smashing.media\/assets\/344dbf88-fdf9-42bb-adb4-46f01eedd629\/b14658fc-bb2d-41a6-8d1a-70eaaf1b8ec8\/touch-design-book-shop-opt.png\"><\/p>\n<\/div>\n<p><\/a>\n<\/div>\n<\/aside>\n<\/div>\n<h2 id=\"add-a-boilerplate-event-handler-for-click-interactions\">Add A Boilerplate Event Handler For Click Interactions<\/h2>\n<p>We want the highlight element to animate when a user changes the <code>.active<\/code> navigation item. Let\u2019s add a <code>click<\/code> event handler to the <code>nav<\/code> element, then filter for events caused only by elements matching our desired selector. In this case, we only want to change the <code>.active<\/code> nav item if the user clicks on a link that does not already have the <code>.active<\/code> class.<\/p>\n<p>Initially, we can call <code>console.log<\/code> to ensure the handler fires only when expected:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">const navbar = document.querySelector('nav');\n\nnavbar.addEventListener('click', function (event) {\n \/\/ return if the clicked element doesn't have the correct selector\n if (!event.target.matches('nav a:not(active)')) {\n return;\n }\n \n console.log('click');\n});\n<\/code><\/pre>\n<\/div>\n<p>Open your browser console and try clicking different items in the navigation bar. You should only see <code>"click"<\/code> being logged when you select a new item in the navigation bar.<\/p>\n<p>Now that we know our event handler is working on the correct elements let\u2019s add code to move the <code>.active<\/code> class to the navigation item that was clicked. We can use the object passed into the event handler to find the element that initialized the event and give that element a class of <code>.active<\/code> after removing it from the previously active item.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">const navbar = document.querySelector('nav');\n\nnavbar.addEventListener('click', function (event) {\n \/\/ return if the clicked element doesn't have the correct selector\n if (!event.target.matches('nav a:not(active)')) {\n return;\n }\n \n- console.log('click');\n+ document.querySelector('nav a.active').classList.remove('active');\n+ event.target.classList.add('active');\n \n});\n<\/code><\/pre>\n<\/div>\n<p>Our <code>#highlight<\/code> element needs to move across the navigation bar and position itself around the active item. Let\u2019s write a function to calculate a new position and width. Since the <code>#highlight<\/code> selector has <code>transition<\/code> styles applied, it will move gradually when its position changes.<\/p>\n<p>Using <code>getBoundingClientRect<\/code>, we can get information about the position and size of an element. We calculate the width of the active navigation item and its offset from the left boundary of the parent element. Then, we assign styles to the highlight element so that its size and position match.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">\/\/ handler for moving the highlight\nconst moveHighlight = () => {\n const activeNavItem = document.querySelector('a.active');\n const highlighterElement = document.querySelector('#highlight');\n \n const width = activeNavItem.offsetWidth;\n\n const itemPos = activeNavItem.getBoundingClientRect();\n const navbarPos = navbar.getBoundingClientRect()\n const relativePosX = itemPos.left - navbarPos.left;\n\n const styles = {\n left: `${relativePosX}px`,\n width: `${width}px`,\n };\n\n Object.assign(highlighterElement.style, styles);\n}\n<\/code><\/pre>\n<\/div>\n<p>Let\u2019s call our new function when the click event fires:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">navbar.addEventListener('click', function (event) {\n \/\/ return if the clicked element doesn't have the correct selector\n if (!event.target.matches('nav a:not(active)')) {\n return;\n }\n \n document.querySelector('nav a.active').classList.remove('active');\n event.target.classList.add('active');\n \n+ moveHighlight();\n});\n<\/code><\/pre>\n<\/div>\n<p>Finally, let\u2019s also call the function immediately so that the border moves behind our initial active item when the page first loads:<\/p>\n<pre><code class=\"language-javascript\">\/\/ handler for moving the highlight\nconst moveHighlight = () => {\n \/\/ ...\n}\n\n\/\/ display the highlight when the page loads\nmoveHighlight();\n<\/code><\/pre>\n<p>Now, the border moves across the navigation bar when a new item is selected. Try clicking the different navigation links to animate the navigation bar.<\/p>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"WbvMxqV\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Moving Highlight Navbar [forked]](https:\/\/codepen.io\/smashingmag\/pen\/WbvMxqV) by <a href=\"https:\/\/codepen.io\/blakeeric\">Blake Lundquist<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/WbvMxqV\">Moving Highlight Navbar [forked]<\/a> by <a href=\"https:\/\/codepen.io\/blakeeric\">Blake Lundquist<\/a>.<\/figcaption><\/figure>\n<p>That only took a few lines of vanilla JavaScript and could easily be extended to account for other interactions, like <code>mouseover<\/code> events. In the next section, we will explore refactoring this feature using the View Transition API.<\/p>\n<div class=\"partners__lead-place\"><\/div>\n<h2 id=\"using-the-view-transition-api\">Using The View Transition API<\/h2>\n<p>The View Transition API provides functionality to create animated transitions between website views. Under the hood, the API creates snapshots of \u201cbefore\u201d and \u201cafter\u201d views and then handles transitioning between them. View transitions are useful for creating animations between documents, providing the <strong>native-app-like user experience<\/strong> featured in frameworks like <a href=\"https:\/\/docs.astro.build\/en\/guides\/view-transitions\/\">Astro<\/a>. However, the API also provides handlers meant for <strong>SPA-style applications<\/strong>. We will use it to reduce the JavaScript needed in our implementation and more easily create fallback functionality.<\/p>\n<p>For this approach, we no longer need a separate <code>#highlight<\/code> element. Instead, we can style the <code>.active<\/code> navigation item directly using pseudo-selectors and let the View Transition API handle the animation between the before-and-after UI states when a new navigation item is clicked.<\/p>\n<p>We\u2019ll start by getting rid of the <code>#highlight<\/code> element and its associated CSS and replacing it with styles for the <code>nav a::after<\/code> pseudo-selector:<\/p>\n<pre><code class=\"language-html\"><nav>\n - <div id=\"highlight\"><\/div>\n <a href=\"#\" class=\"active\">Home<\/a>\n <a href=\"#services\">Services<\/a>\n <a href=\"#about\">About<\/a>\n <a href=\"#contact\">Contact<\/a>\n<\/nav>\n<\/code><\/pre>\n<pre><code class=\"language-css\">- #highlight {\n- z-index: 0;\n- position: absolute;\n- height: 100%;\n- width: 0;\n- left: 0;\n- box-sizing: border-box;\n- transition: all 0.2s ease;\n- }\n\n+ nav a::after {\n+ content: \" \";\n+ position: absolute;\n+ left: 0;\n+ top: 0;\n+ width: 100%;\n+ height: 100%;\n+ border: none;\n+ box-sizing: border-box;\n+ }\n<\/code><\/pre>\n<p>For the <code>.active<\/code> class, we include the <code>view-transition-name<\/code> property, thus unlocking the magic of the View Transition API. Once we trigger the view transition and change the location of the <code>.active<\/code> navigation item in the DOM, \u201cbefore\u201d and \u201cafter\u201d snapshots will be taken, and the browser will animate the border across the bar. We\u2019ll give our view transition the name of <code>highlight<\/code>, but we could theoretically give it any name.<\/p>\n<pre><code class=\"language-css\">nav a.active::after {\n border: 2px solid green;\n view-transition-name: highlight;\n}\n<\/code><\/pre>\n<p>Once we have a selector that contains a <code>view-transition-name<\/code> property, the only remaining step is to trigger the transition using the <code>startViewTransition<\/code> method and pass in a callback function.<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">const navbar = document.querySelector('nav');\n\n\/\/ Change the active nav item on click\nnavbar.addEventListener('click', async function (event) {\n\n if (!event.target.matches('nav a:not(.active)')) {\n return;\n }\n \n document.startViewTransition(() => {\n document.querySelector('nav a.active').classList.remove('active');\n\n event.target.classList.add('active');\n });\n});\n<\/code><\/pre>\n<\/div>\n<p>Above is a revised version of the <code>click<\/code> handler. Instead of doing all the calculations for the size and position of the moving border ourselves, the View Transition API handles all of it for us. We only need to call <code>document.startViewTransition<\/code> and pass in a callback function to change the item that has the <code>.active<\/code> class!<\/p>\n<div class=\"partners__lead-place\"><\/div>\n<h2 id=\"adjusting-the-view-transition\">Adjusting The View Transition<\/h2>\n<p>At this point, when clicking on a navigation link, you\u2019ll notice that the transition works, but some strange sizing issues are visible.<\/p>\n<figure><a href=\"https:\/\/files.smashing.media\/articles\/creating-moving-highlight-navigation-bar-vanilla-javascript\/2-view-transition-sizing-issues.gif\"><img loading=\"lazy\" src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" width=\"800\" height=\"163\" alt=\"The view transition with sizing issues\" class=\"lazyload\" data-src=\"http:\/\/www.computercoursesonline.com\/wp-content\/uploads\/2025\/06\/2-view-transition-sizing-issues-800px.gif\"><\/a><figcaption>(<a href=\"https:\/\/files.smashing.media\/articles\/creating-moving-highlight-navigation-bar-vanilla-javascript\/2-view-transition-sizing-issues.gif\">Large preview<\/a>)<\/figcaption><\/figure>\n<p>This sizing inconsistency is caused by aspect ratio changes during the course of the view transition. We won\u2019t go into detail here, but <a href=\"https:\/\/jakearchibald.com\/2024\/view-transitions-handling-aspect-ratio-changes\/\">Jake Archibald has a detailed explanation you can read<\/a> for more information. In short, to ensure the height of the border stays uniform throughout the transition, we need to declare an explicit <code>height<\/code> for the <code>::view-transition-old<\/code> and <code>::view-transition-new<\/code> pseudo-selectors representing a static snapshot of the old and new view, respectively.<\/p>\n<pre><code class=\"language-css\">::view-transition-old(highlight) {\n height: 100%;\n}\n\n::view-transition-new(highlight) {\n height: 100%;\n}\n<\/code><\/pre>\n<p>Let\u2019s do some final refactoring to tidy up our code by moving the callback to a separate function and adding a fallback for when view transitions aren\u2019t supported:<\/p>\n<div class=\"break-out\">\n<pre><code class=\"language-javascript\">const navbar = document.querySelector('nav');\n\n\/\/ change the item that has the .active class applied\nconst setActiveElement = (elem) => {\n document.querySelector('nav a.active').classList.remove('active');\n elem.classList.add('active');\n}\n\n\/\/ Start view transition and pass in a callback on click\nnavbar.addEventListener('click', async function (event) {\n if (!event.target.matches('nav a:not(.active)')) {\n return;\n }\n\n \/\/ Fallback for browsers that don't support View Transitions:\n if (!document.startViewTransition) {\n setActiveElement(event.target);\n return;\n }\n \n document.startViewTransition(() => setActiveElement(event.target));\n});\n<\/code><\/pre>\n<\/div>\n<p>Here\u2019s our view transition-powered navigation bar! Observe the smooth transition when you click on the different links.<\/p>\n<figure class=\"break-out\">\n<p data-height=\"480\" data-theme-id=\"light\" data-slug-hash=\"ogXELKE\" data-user=\"smashingmag\" data-default-tab=\"result\" class=\"codepen\">See the Pen [Moving Highlight Navbar with View Transition [forked]](https:\/\/codepen.io\/smashingmag\/pen\/ogXELKE) by <a href=\"https:\/\/codepen.io\/blakeeric\">Blake Lundquist<\/a>.<\/p><figcaption>See the Pen <a href=\"https:\/\/codepen.io\/smashingmag\/pen\/ogXELKE\">Moving Highlight Navbar with View Transition [forked]<\/a> by <a href=\"https:\/\/codepen.io\/blakeeric\">Blake Lundquist<\/a>.<\/figcaption><\/figure>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>Animations and transitions between website UI states used to require many kilobytes of external libraries, along with verbose, confusing, and error-prone code, but vanilla JavaScript and CSS have since incorporated features to achieve <strong>native-app-like interactions without breaking the bank<\/strong>. We demonstrated this by implementing the \u201cmoving highlight\u201d navigation pattern using two approaches: CSS transitions combined with the <code>getBoundingClientRect()<\/code> method and the View Transition API.<\/p>\n<h3 id=\"resources\">Resources<\/h3>\n<ul>\n<li><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Element\/getBoundingClientRect\"><code>getBoundingClientRect()<\/code> method documentation<\/a><\/li>\n<li><a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/View_Transition_API\">View Transition API documentation<\/a><\/li>\n<li>\u201c<a href=\"https:\/\/jakearchibald.com\/2024\/view-transitions-handling-aspect-ratio-changes\/\">View Transitions: Handling Aspect Ratio Changes<\/a>\u201d by Jake Archibald<\/li>\n<\/ul>\n<div class=\"signature\">\n <img src=\"data:image\/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\" alt=\"Smashing Editorial\" width=\"35\" height=\"46\" loading=\"lazy\" class=\"lazyload\" data-src=\"https:\/\/www.smashingmagazine.com\/images\/logo\/logo--red.png\"><br \/>\n <span>(gg, yk)<\/span>\n<\/div>\n<\/article>\n","protected":false},"excerpt":{"rendered":"<p>Creating The &ldquo;Moving Highlight&rdquo; Navigation Bar With JavaScript And CSS Creating The &ldquo;Moving Highlight&rdquo; Navigation Bar With JavaScript And CSS Blake Lundquist 2025-06-11T13:00:00+00:00 2025-06-12T20:33:08+00:00 I recently came across an old jQuery tutorial demonstrating a \u201cmoving highlight\u201d navigation bar and decided the concept was due for a modern upgrade. With this pattern, the border around the…<\/p>\n","protected":false},"author":1,"featured_media":943,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[14],"tags":[],"_links":{"self":[{"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/posts\/941"}],"collection":[{"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/comments?post=941"}],"version-history":[{"count":3,"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/posts\/941\/revisions"}],"predecessor-version":[{"id":946,"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/posts\/941\/revisions\/946"}],"wp:featuredmedia":[{"embeddable":true,"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/media\/943"}],"wp:attachment":[{"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/media?parent=941"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/categories?post=941"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.computercoursesonline.com\/index.php\/wp-json\/wp\/v2\/tags?post=941"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}