Responsive Nav

Responsive navigation is a menu that adapts to screen size, typically showing a full horizontal bar on wide screens and collapsing into a tappable hamburger drawer on mobile, all while staying keyboard-accessible and screen-reader friendly.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

By the end of this lesson you'll be able to build a navbar that switches from a horizontal flexbox row on desktop to an accessible hamburger drawer on mobile — with a keyboard-usable toggle, a sticky header, and screen-reader support that actually works.

A responsive nav is like a wall map versus a folding pocket map. In a big lobby (desktop) you hang the whole map flat on the wall so every destination is visible at a glance — that's your horizontal flexbox row.

Out walking (mobile) you can't carry a wall, so the same map folds into a pocket and you unfold it on request . The hamburger button is the fold: tap it and the drawer opens, tap again and it tucks away. The content is identical — only the presentation changes with the space you have. And just like a good folding map snaps shut cleanly, your menu must close properly: nothing should be left half-open where a finger, or a keyboard, can still reach a link that is meant to be hidden.

Almost every navbar is a flex container. You put the logo and the link list in one row, then use justify-content: space-between to push them to opposite ends and align-items: center to line them up vertically. The links themselves are a second flex row with a gap between them. Mark the whole thing up as nav with a ul of links — that semantic structure is what screen readers announce as "navigation".

Run the worked example. Resize the preview wide and narrow — at this stage the row never collapses; you're just seeing flexbox do the horizontal layout.

A sticky header stays pinned to the top while the page scrolls underneath it. You get it with two declarations: position: sticky and a top: 0 threshold that says "stick when you reach the top edge". Add a z-index so the header sits above the content it scrolls over.

sticky is not the same as fixed : a sticky element stays in the document flow and only "sticks" once you scroll to its threshold, so it doesn't need a manual padding-top spacer the way a fixed bar does. Watch out for one trap — sticky breaks if any ancestor has overflow: hidden .

On mobile the horizontal links don't fit, so you hide them and show a hamburger button (the ☰ icon) that opens them on demand. The single most important rule: the toggle must be a real button , not a div . A button is focusable, fires on Enter and Space for free, and is announced as a button — a div is none of those things.

Wire up the accessibility with three attributes: aria-controls names the menu it opens, aria-expanded reports open or closed (you must flip it in JavaScript every toggle), and aria-label gives the icon-only button a name. When the menu closes it goes to display: none so its links leave the tab order — a hidden link must never stay focusable.

A polished mobile menu often slides in from the side as a drawer (also called off-canvas). You park the panel off-screen with transform: translateX(-100%) and slide it back to translateX(0) when open. Animating transform (not left ) keeps the motion smooth.

Because the open drawer covers the page, a keyboard user could otherwise tab into the hidden content behind it. The accessible answer is to remove the drawer from the tab order while it is closed — here the closed drawer carries visibility: hidden , which both hides it and pulls its links out of focus order, then flips to visible when open. A dimmed backdrop also closes the drawer when clicked.

You can open a menu with zero JavaScript using the "checkbox hack". A hidden input type="checkbox" stores the open/closed state, a label toggles it, and the :checked pseudo-class reveals the menu in pure CSS. It's clever and works offline.

The trade-off is accessibility. A checkbox is announced as a checkbox, not a menu button, and you can't easily expose aria-expanded or wire up Escape. So treat the checkbox pattern as a fallback; for real products prefer the JavaScript button from section 3. The example below shows the pure-CSS version so you can see the mechanism.

The menu opens and closes, but the screen-reader wiring is missing. Fill in the blanks marked ___ so the button reports its state, then run it and check the expected result in the comments.

This navbar shows the links on every width and the hamburger is always hidden. Add a media query so that below 768px the links hide and the hamburger appears. Fill in the blanks, then resize the preview to check.

Support is faded now — only an outline is given. Build a sticky navbar that is horizontal on desktop and collapses to an accessible hamburger drawer on mobile. Use the worked examples in sections 2, 3 and 4 as your reference if you get stuck.

You can now build a navbar that works on every screen and for every user. The essentials:

Next up: Modern Layouts , where you'll combine grid and flexbox into full page structures.

Practice quiz

Which flexbox declarations create the classic 'logo left, links right, vertically centered' navbar?

  • display: grid; place-items: center
  • display: block; text-align: center
  • display: flex; justify-content: space-between; align-items: center
  • display: inline-flex; gap: auto

Answer: display: flex; justify-content: space-between; align-items: center. justify-content: space-between pushes the logo and links to opposite ends, and align-items: center lines them up vertically.

What two declarations make a header stick to the top while the page scrolls?

  • position: sticky; top: 0
  • position: fixed; bottom: 0
  • position: absolute; top: 0
  • display: sticky; z-index: 0

Answer: position: sticky; top: 0. position: sticky plus a top: 0 threshold pins the header once it reaches the top edge, while staying in the document flow.

A common reason position: sticky silently fails is that an ancestor has which property?

  • display: flex
  • margin: 0 auto
  • z-index: 100
  • overflow: hidden (or auto/scroll)

Answer: overflow: hidden (or auto/scroll). An ancestor with overflow: hidden/auto/scroll creates a scroll container the sticky element sticks inside, breaking the expected behaviour.

Why must a hamburger toggle be a real <button> rather than a <div>?

  • Divs cannot have a background
  • A button is focusable, fires on Enter/Space, and is announced as a button
  • Buttons load faster
  • Divs cannot be clicked

Answer: A button is focusable, fires on Enter/Space, and is announced as a button. A real <button> is keyboard-focusable, responds to Enter and Space for free, and is announced as a button to screen readers — a div is none of those.

What does the aria-expanded attribute communicate on a menu toggle?

  • Whether the controlled menu is currently open or closed
  • The menu's animation speed
  • The menu's width
  • The number of menu items

Answer: Whether the controlled menu is currently open or closed. aria-expanded reports open (true) or closed (false). You must update it in JavaScript on every toggle so assistive tech stays in sync.

Which ARIA attribute names the element a toggle button opens?

  • aria-label
  • aria-hidden
  • aria-controls
  • aria-live

Answer: aria-controls. aria-controls points at the id of the menu the button opens, linking the trigger to the controlled element.

Why hide a closed mobile menu with display: none rather than just opacity: 0?

  • opacity is slower
  • display: none removes the links from the tab order; opacity: 0 leaves them focusable
  • opacity: 0 is invalid CSS
  • display: none animates better

Answer: display: none removes the links from the tab order; opacity: 0 leaves them focusable. An opacity: 0 menu is invisible but still in the tab order, so keyboard users tab into hidden links. display: none (or visibility: hidden) removes them from focus order.

What transform parks an off-canvas drawer off-screen to the left, ready to slide in?

  • translateY(-100%)
  • scale(0)
  • rotate(-90deg)
  • translateX(-100%)

Answer: translateX(-100%). translateX(-100%) pushes the drawer fully off the left edge; animating transform back to translateX(0) slides it in smoothly.

In the CSS-only 'checkbox hack' menu, what reveals the menu?

  • A :hover rule on the body
  • The :checked pseudo-class on the hidden checkbox
  • An onclick attribute
  • A media query

Answer: The :checked pseudo-class on the hidden checkbox. A hidden <input type="checkbox"> holds the state and #toggle:checked ~ .menu { display: block; } reveals the menu in pure CSS.

What is the minimum keyboard behaviour every toggle menu should provide?

  • Arrow keys to scroll
  • Tab disables the menu
  • Escape closes the menu and returns focus to the trigger button
  • Spacebar reloads the page

Answer: Escape closes the menu and returns focus to the trigger button. At minimum, pressing Escape should close the menu and send focus back to the button that opened it.