Navigation Bars
The navigation bar is the part of a website people use most and notice least. When the links are tiny, the current page is unclear or the menu falls apart on a phone, visitors leave.
In this lesson you will build navigation bars from the ground up: the right HTML, a horizontal bar, a vertical sidebar, a header that sticks to the top, and a menu that folds behind a button on small screens. Along the way you will combine Flexbox, Positioning and the Popover API.
Start with the right HTML
A navigation bar is a list of links, so mark it up as one. Wrap the list in a <nav> element and give the <nav> a short aria-label:
<nav class="main-nav" aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/drinks" aria-current="page">Drinks</a></li>
<li><a href="/snacks">Snacks</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>Each part has a job:
<nav>tells screen readers and search engines "this is a block of navigation links". Screen reader users can jump straight to it.aria-label="Main"gives the navigation a name. Most sites have more than one<nav>(main menu, footer links, breadcrumbs), and the label tells them apart.<ul>lets a screen reader announce "list, 4 items", so the user knows how many choices there are before hearing them.aria-current="page"marks the link for the page the visitor is on. Screen readers announce it as "current page".
Notice that the HTML has no class="active". The aria-current attribute already says which link is current, and CSS can select it directly. One attribute serves both the screen reader and the styling, so they can never disagree.
A horizontal navigation bar
Now turn the list into a row. The steps are always the same: remove the list bullets and spacing, make the list a flex container, and give the links padding.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Horizontal navigation</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
.site-header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1.5rem;
padding: 0.5rem 1rem;
background: #fff4e6;
border-bottom: 1px solid #e8cfb0;
}
.brand img { display: block; width: auto; height: 3rem; }
.main-nav ul {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.main-nav a {
display: block;
padding: 0.6rem 0.9rem;
border-radius: 6px;
color: #5a2e0c;
font-weight: 600;
text-decoration: none;
}
.main-nav a:hover { background: #fde2c4; }
.main-nav a:focus-visible {
outline: 3px solid #1d4ed8;
outline-offset: 2px;
}
.main-nav a[aria-current="page"] {
background: #7a3e10;
color: white;
}
main { padding: 1rem; }
</style>
</head>
<body>
<header class="site-header">
<a class="brand" href="/"><img src="https://course.ethicscomputer.in/assets/samples/logo.svg" alt="Chai Corner home" width="240" height="80"></a>
<nav class="main-nav" aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/drinks" aria-current="page">Drinks</a></li>
<li><a href="/snacks">Snacks</a></li>
<li><a href="/order">Order online</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<main>
<h1>Drinks</h1>
<p>Masala chai ₹30, ginger chai ₹35, cold coffee ₹60.</p>
</main>
</body>
</html>Here is why each rule is there:
list-style: none,margin: 0andpadding: 0remove the bullets and the default indent of the list.display: flexwithgaplines the links up in a row with even spacing.flex-wrap: wrapon both the header and the list lets links drop to a new line on a narrow screen instead of overflowing.- The padding is on the
<a>, not on the<li>. The whole padded area becomes clickable, which gives fingers on a phone a much bigger target.display: blockmakes the link fill that area. a[aria-current="page"]is an attribute selector that finds the current link. It gets a filled background, a change of shape as well as colour, so people who cannot tell colours apart still see it.
Press Tab in the result to move through the links: each one shows a blue ring, thanks to :focus-visible. Clicking a link only prints a note, because the Try-it editor does not load other pages.
A vertical navigation bar
Sidebars in dashboards, account pages and documentation use a vertical list. It is even simpler: list items are already stacked, so you mostly style the links.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vertical navigation</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
.layout { display: grid; grid-template-columns: 12rem 1fr; min-height: 100vh; }
.side-nav { padding-block: 1rem; background: #f1f5f9; }
.side-nav h2 { margin: 0 1rem 0.5rem; font-size: 0.85rem; text-transform: uppercase; color: #475569; }
.side-nav ul { margin: 0; padding: 0; list-style: none; }
.side-nav a {
display: block;
padding: 0.6rem 1rem;
border-inline-start: 4px solid transparent;
color: #1e293b;
text-decoration: none;
}
.side-nav a:hover { background: #e2e8f0; }
.side-nav a:focus-visible { outline: 3px solid #1d4ed8; outline-offset: -3px; }
.side-nav a[aria-current="page"] {
border-inline-start-color: #1d4ed8;
background: white;
font-weight: 700;
}
main { padding: 1rem 1.5rem; }
</style>
</head>
<body>
<div class="layout">
<nav class="side-nav" aria-label="Account">
<h2>My account</h2>
<ul>
<li><a href="/account/profile">Profile</a></li>
<li><a href="/account/orders" aria-current="page">Orders</a></li>
<li><a href="/account/addresses">Saved addresses</a></li>
<li><a href="/account/payments">Payment methods</a></li>
</ul>
</nav>
<main>
<h1>Your orders</h1>
<p>Rohan, your order of two notebooks (₹180) will reach Lucknow on Friday.</p>
</main>
</div>
</body>
</html>Two details are worth copying. Every link has a transparent 4px border on its starting side, so the current link can change only the border colour without making the text jump sideways. And the focus ring uses a negative outline-offset, which draws it just inside the link, where it cannot be cut off by the edge of the sidebar. The border-inline-start property is the logical version of border-left, explained in Logical Properties.
A sticky header
A header that stays at the top while the page scrolls keeps the menu within reach on long pages. Two lines of CSS do it:
.site-header {
position: sticky;
top: 0; /* where it sticks */
background: #fff4e6; /* a solid background, so text does not show through */
}Without a background, page content scrolls underneath the header and shows through it. If the header seems to scroll away anyway, check the reasons listed in Positioning: no top value, or an ancestor with overflow: hidden. When links jump to a heading further down the page, the sticky header can cover that heading; scroll-padding-top fixes this, as shown in Scroll Snap & Scroll Effects.
Collapse the menu on small screens
With four or five short links, wrapping (as in the first example) is often all you need. With more links, a common pattern is to hide them behind a "Menu" button on phones and show them in a row on wider screens. The popover attribute gives you this without any JavaScript: the button opens the list, and the list closes when the user presses Esc or taps outside it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Responsive navigation</title>
<style>
:root { --bar: 4rem; }
body { margin: 0; font-family: system-ui, sans-serif; }
.site-header {
position: sticky;
top: 0;
display: flex;
align-items: center;
justify-content: space-between;
min-height: var(--bar);
padding-inline: 1rem;
background: #fff4e6;
border-bottom: 1px solid #e8cfb0;
}
.brand img { display: block; width: auto; height: 3rem; }
.menu-btn {
padding: 0.5rem 1rem;
border: 2px solid #7a3e10;
border-radius: 6px;
background: white;
color: #7a3e10;
font: inherit;
font-weight: 600;
}
/* Phones: the list is a popover panel just under the bar */
.menu {
inset: var(--bar) 0 auto;
width: auto;
margin: 0;
padding: 0.5rem;
border: 0;
background: #fff4e6;
box-shadow: 0 8px 16px rgb(0 0 0 / 0.15);
list-style: none;
}
.menu a {
display: block;
padding: 0.75rem 1rem;
border-radius: 6px;
color: #5a2e0c;
font-weight: 600;
text-decoration: none;
}
.menu a:hover { background: #fde2c4; }
.menu a:focus-visible { outline: 3px solid #1d4ed8; outline-offset: -3px; }
.menu a[aria-current="page"] { background: #7a3e10; color: white; }
/* Wider screens: hide the button and show the list in the header */
@media (min-width: 40rem) {
.menu-btn { display: none; }
.menu {
display: flex;
position: static;
gap: 0.25rem;
padding: 0;
background: none;
box-shadow: none;
}
}
main { padding: 1rem; }
section { min-height: 70vh; }
</style>
</head>
<body>
<header class="site-header">
<a class="brand" href="/"><img src="https://course.ethicscomputer.in/assets/samples/logo.svg" alt="Chai Corner home" width="240" height="80"></a>
<nav aria-label="Main">
<button class="menu-btn" type="button" popovertarget="site-menu">Menu</button>
<ul class="menu" id="site-menu" popover>
<li><a href="/">Home</a></li>
<li><a href="/drinks" aria-current="page">Drinks</a></li>
<li><a href="/snacks">Snacks</a></li>
<li><a href="/order">Order online</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<main>
<section><h1>Drinks</h1><p>Scroll down: the header stays at the top.</p></section>
<section><h2>Cold drinks</h2><p>Cold coffee ₹60, lassi ₹50, nimbu pani ₹25.</p></section>
</main>
</body>
</html>Make the result narrow and you get a "Menu" button that opens a panel under the bar. Make it wider than 40rem (640px) and the button disappears while the links sit in a row. Here is how it works:
- On phones, the
<ul>is a popover. The browser hides it until the button opens it, then draws it in the top layer above the page, so noz-indexis needed. The.menurule moves it from the default centre of the window to just under the bar. - A popover is hidden by a
display: nonerule in the browser's own stylesheet. Your styles beat the browser's, so inside the media querydisplay: flexshows the list even when the popover is closed, andposition: staticputs it back into the header. - Esc, tapping outside and keyboard support for the button all come from the browser.
Focus styles and a skip link
Keyboard users move through a page with Tab, and they need to see where they are. Give every link a clear :focus-visible outline, as all the examples above do, and never remove outlines with outline: none unless you add something just as visible.
A navigation bar with many links has one more cost: keyboard users must tab through all of them on every page before reaching the content. A skip link solves this. It is the first link on the page, hidden above the screen until it receives focus:
.skip-link {
position: absolute;
top: 0;
left: 1rem;
z-index: 10;
padding: 0.5rem 1rem;
background: #1d4ed8;
color: white;
translate: 0 -120%; /* parked just above the screen */
}
.skip-link:focus {
translate: 0 0; /* slides into view when a keyboard user reaches it */
}The HTML is <a class="skip-link" href="#main">Skip to main content</a> at the very top of <body>, and id="main" on the <main> element.
Common mistakes
- Links without padding. A link that is only as big as its text is hard to tap. Put padding on the
<a>and make itdisplay: block. - Showing the current page with colour alone. Add a background, an underline or a border as well, and mark it with
aria-current="page". - A sticky header with no background. Content shows through it while scrolling.
- Removing focus outlines. Style
:focus-visibleinstead of hiding it. - Setting
displayon a popover menu everywhere. It stays visible on phones too. Only override it inside the wide-screen media query.
Key points
- Build navigation as
<nav aria-label="...">containing a<ul>of links. - Mark the current page with
aria-current="page"and style it witha[aria-current="page"]. - A flex list with
gapandflex-wrap: wrapmakes a horizontal bar; padded block links make big targets. position: sticky; top: 0plus a solid background keeps the header in view while scrolling.- A popover list behind a "Menu" button collapses the navigation on phones without JavaScript.
- Give every link a visible
:focus-visiblestyle, and add a skip link when the menu is long.
Exercise
Build a header for a cricket academy in Jaipur. Put the academy name on the left and four links on the right: Home, Coaching, Fees and Contact. Mark Fees as the current page with aria-current, and show it with a thick bottom border, not only a colour change. Make the header sticky, let the links wrap on narrow screens, and give every link a visible focus ring.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cricket academy</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; }
header {
position: sticky;
top: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem 1rem;
padding: 0.5rem 1rem;
background: #14532d;
color: white;
}
nav ul { display: flex; flex-wrap: wrap; gap: 0.25rem; margin: 0; padding: 0; list-style: none; }
nav a {
display: block;
padding: 0.6rem 0.8rem;
border-bottom: 4px solid transparent;
color: white;
text-decoration: none;
}
nav a:hover { background: #166534; }
nav a:focus-visible { outline: 3px solid #facc15; outline-offset: 2px; }
nav a[aria-current="page"] { border-bottom-color: #facc15; font-weight: 700; }
main { min-height: 150vh; padding: 1rem; }
</style>
</head>
<body>
<header>
<strong>Pink City Cricket Academy</strong>
<nav aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/coaching">Coaching</a></li>
<li><a href="/fees" aria-current="page">Fees</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<main>
<h1>Fees</h1>
<p>Junior batch: ₹2,500 a month. Senior batch: ₹3,500 a month.</p>
</main>
</body>
</html>Every link has a transparent bottom border, so the current link only changes its border colour and nothing shifts. The yellow focus ring stands out clearly against the dark green header.
Quick Quiz
1. Which attribute tells screen readers which link points to the page they are on?
aria-current="page" is announced as "current page", and CSS can select it with a[aria-current="page"].
2. Why put the padding on the <a> element instead of the <li>?
Padding on the link makes the link itself bigger, so the clickable target grows.
3. A header has position: sticky but scrolls away with the page. What is the most likely cause?
A sticky element needs an offset such as top: 0 to know where to stick.
4. A <ul popover> should appear as a normal row of links on wide screens. What does that?
Your display rule overrides the browser's display: none for closed popovers, and position: static puts the list back into the header.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
