Skip to main content
    Courses/HTML & CSS/Responsive Navigation

    Lesson 40 • Advanced Track

    Responsive Navigation Patterns

    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.

    What You'll Learn

    Lay out a horizontal navbar with flexbox and collapse it on mobile
    Build a hamburger button with aria-expanded and aria-controls
    Toggle an off-canvas drawer that is keyboard and screen-reader usable
    Make a header stick to the top with position: sticky
    Close the menu with Escape and return focus to the button
    Compare the CSS-only checkbox pattern with a JavaScript toggle
    Before this lesson: you should be comfortable with flexbox and media queries, and know what a CSS pseudo-class like :checked is. If flexbox or breakpoints feel new, review Flexbox Layout and Responsive Design first.

    💡 Think of It Like This

    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.

    1. The Horizontal Navbar (Flexbox)

    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.

    Worked example: a flexbox navbar

    Logo on the left, links on the right, centred vertically

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Flexbox navbar</title>
      <style>
        * { margin: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; }
    
        /* The navbar is a flex row. */
        .navbar {
          display: flex;
          justify-content: space-between; /* logo to the left, links to the right */
          align-items: center;           /* everything centred on the same line */
          padding: 12px 24px;
          background: #1976D2;
          color: w
    ...

    2. Make the Header Sticky

    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.

    Worked example: sticky header

    The bar stays at the top as the page scrolls

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Sticky header</title>
      <style>
        * { margin: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; }
    
        .navbar {
          position: sticky;   /* stays in flow, then sticks... */
          top: 0;             /* ...once its top edge hits the viewport top */
          z-index: 100;       /* sit above the scrolling content */
          display: flex;
          justify-content: space-between;
          align-items: center
    ...

    3. The Accessible Hamburger Toggle

    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.

    Worked example: accessible hamburger menu

    A real button with aria-expanded, aria-controls, and Escape to close

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Accessible hamburger</title>
      <style>
        * { margin: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; }
    
        .navbar {
          display: flex; justify-content: space-between; align-items: center;
          padding: 12px 24px; background: #1976D2; color: white;
        }
        .logo { font-weight: 800; font-size: 1.2rem; }
    
        /* The tog
    ...

    4. The Off-Canvas Drawer

    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.

    Worked example: sliding off-canvas drawer

    Translate the panel in from the left, with a backdrop and Escape

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Off-canvas drawer</title>
      <style>
        * { margin: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; }
    
        .navbar {
          display: flex; align-items: center; gap: 12px;
          padding: 12px 24px; background: #1976D2; color: white;
        }
        .hamburger {
          background: none; border: none; color: white;
          font-size: 1.6r
    ...

    5. CSS-Only Checkbox Toggle vs JavaScript

    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.

    Worked example: the CSS-only checkbox menu

    No JavaScript — :checked reveals the menu

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Checkbox-hack menu</title>
      <style>
        * { margin: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; }
    
        .navbar { background: #1976D2; color: white; padding: 12px 24px;
                  display: flex; align-items: center; gap: 12px; }
        .logo { font-weight: 800; }
    
        /* 1) The checkbox holds the state but is hidden from 
    ...

    🎯 Your Turn #1 — Wire up the ARIA on a toggle

    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.

    Your Turn #1: add aria-controls and keep aria-expanded in sync

    Name the menu, then flip aria-expanded on every toggle

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Your Turn 1</title>
      <style>
        * { margin: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; }
        .navbar { background:#1976D2; color:white; padding:12px 24px;
                  display:flex; justify-content:space-between; align-items:center; }
        .logo { font-weight:800; }
        .hamburger { background:none; border:none; color:
    ...

    🎯 Your Turn #2 — Collapse the navbar at a breakpoint

    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.

    Your Turn #2: swap links for the hamburger on mobile

    A max-width media query hides .nav-links and shows .hamburger

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Your Turn 2</title>
      <style>
        * { margin: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; }
        .navbar { background:#1976D2; color:white; padding:12px 24px;
                  display:flex; justify-content:space-between; align-items:center; }
        .logo { font-weight:800; }
        .nav-links { display:flex; gap:20px; list-style:n
    ...

    🧩 Mini-Challenge — A complete responsive navbar

    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.

    Mini-Challenge: responsive navbar from scratch

    Sticky + flexbox + accessible hamburger toggle

    Try it Yourself »
    Code Preview
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Mini-Challenge</title>
      <style>
        /* 🧩 MINI-CHALLENGE: a full responsive navbar
           1. Style .navbar as a sticky flex row (position: sticky; top: 0;
                display: flex; justify-content: space-between; align-items: center)
           2. Style .nav-links as a horizontal flex row with a gap; hide
                list bullets
           3. Hide .hamburger
    ...

    ⚠️ Common Errors (and the fix)

    • No aria-expanded (or it never updates). A screen-reader user can't tell whether the menu is open. Setting aria-expanded="false" once in the HTML isn't enough either. Fix: call btn.setAttribute('aria-expanded', open ? 'true' : 'false') inside the toggle handler so it tracks the real state.
    • Hidden but still focusable. Hiding a menu with only opacity: 0 (or moving it off-screen) leaves its links in the tab order — keyboard users tab into an invisible menu. Fix: close with display: none or visibility: hidden, which remove the links from focus order too.
    • Not keyboard accessible. Using a <div> or <span> with an onclick for the toggle can't be reached by Tab and ignores Enter/Space. Fix: use a real <button> — it's focusable and fires on both keys automatically.
    • Missing focus trap / no Escape. With a full-screen drawer open, Tab can wander into the page hidden behind it, and there's no quick way out. Fix: at minimum, let Escape close the drawer and return focus to the button; for a covering overlay, also keep Tab cycling inside the open menu (a focus trap).
    • Sticky header won't stick. position: sticky; top: 0; does nothing if an ancestor has overflow: hidden/auto/scroll or you forgot the top threshold. Fix: remove the overflow rule on parents and make sure you set top: 0.

    📋 Quick Reference

    GoalUse this
    Horizontal navbardisplay: flex; justify-content: space-between; align-items: center;
    Space the links.nav-links { display: flex; gap: 24px; }
    Sticky headerposition: sticky; top: 0; z-index: 100;
    Accessible toggle<button aria-controls="menu" aria-expanded="false">
    Keep ARIA in syncbtn.setAttribute('aria-expanded', open ? 'true' : 'false')
    Collapse on mobile@media (max-width: 768px) { .nav-links { display: none; } }
    Off-canvas drawertransform: translateX(-100%)translateX(0)
    Hide and unfocusdisplay: none / visibility: hidden
    CSS-only toggle#nav-toggle:checked ~ .menu { display: block; }

    ❓ Frequently Asked Questions

    Why does aria-expanded matter on a hamburger button?

    A sighted user can see whether the menu is open or shut. A screen-reader user cannot — all they hear is the button's name. aria-expanded="false" tells assistive tech the button controls something that is currently collapsed; flipping it to "true" when the menu opens announces "expanded". Without it the user has no idea the button did anything, or whether the menu is already open. You must update the attribute in JavaScript every time you toggle the menu — setting it once in the HTML is not enough.

    Should I use display:none or visibility/opacity to hide the mobile menu?

    Both fully remove the element from the accessibility tree and the tab order, which is what you want for a closed menu — a hidden link must not be focusable. display:none is the simplest and most reliable. The catch is that display cannot be animated, so if you want a slide or fade you animate transform/opacity for the motion and still toggle a state that removes it from focus when closed (for example a hidden attribute or display:none at the end of the transition). Never hide a menu with only opacity:0 or visibility:hidden left half-on, because the links stay clickable and tabbable while invisible.

    What is a focus trap and do I need one?

    A focus trap keeps keyboard focus inside an open overlay so Tab cycles through the menu's links instead of escaping to the page behind it. You need one for a full-screen or off-canvas drawer that visually covers the page — otherwise a keyboard user tabs into content they cannot see. For a simple dropdown that sits in the flow you usually do not trap focus; you just make sure Escape closes it and returns focus to the button. The minimum every toggle menu needs is: Escape closes it, and focus returns to the trigger button on close.

    Can I build a mobile menu with no JavaScript at all?

    Yes — the "checkbox hack" uses a hidden <input type="checkbox"> plus a <label> as the toggle, and the :checked pseudo-class reveals the menu in pure CSS. It works without JS, which is its appeal. The downside is accessibility: a checkbox is announced as a checkbox, not a menu button, and you cannot easily expose aria-expanded state or wire up Escape. For production, a real <button> with a few lines of JavaScript is the more accessible choice; reach for the checkbox hack only when JS is genuinely unavailable.

    Why is my sticky header not sticking?

    position: sticky only works if the element has a threshold to stick to (you set top: 0) and none of its ancestors clip or hide overflow. The most common cause is an ancestor with overflow: hidden, auto, or scroll — that creates a scroll container the sticky element sticks inside of, which usually is not the page. Also check the element is a direct, full-height-enough child of the scrolling area and that you actually gave it top: 0. Remove stray overflow rules on parents first.

    Where should the hamburger menu break to mobile?

    There is no magic number — the right breakpoint is wherever your horizontal links stop fitting comfortably, which you find by shrinking the window until they crowd or wrap. A common starting point is around 768px, but a nav with eight long items may need to switch sooner and a nav with three short items can stay horizontal longer. Design mobile-first: write the small-screen layout as the default, then add a min-width media query that lays the links out horizontally once there is room.

    🎉 Lesson Complete

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

    • ✅ A navbar is a flexbox rowspace-between + align-items: center
    • position: sticky; top: 0; pins the header while the page scrolls
    • ✅ The mobile toggle must be a real <button> with aria-controls and aria-expanded
    • ✅ Update aria-expanded on every toggle, and let Escape close + return focus
    • ✅ Hide closed menus with display: none/visibility: hidden so links leave the tab order
    • ✅ A drawer slides with transform: translateX; the checkbox hack is a no-JS fallback, not the accessible default

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

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service