Building navigation
Top bars, responsive menus, off-canvas panels, dropdowns, breadcrumbs and pagination — with the initialisation details that make them behave on touch.
The top bar and responsive menu
<div class="top-bar" id="siteNav">
<div class="top-bar-left">
<ul class="dropdown menu" data-dropdown-menu>
<li class="menu-text">Acme</li>
<li>
<a href="/product/">Product</a>
<ul class="menu vertical">
<li><a href="/product/grid/">Grid</a></li>
<li><a href="/product/forms/">Forms</a></li>
</ul>
</li>
<li><a href="/pricing/">Pricing</a></li>
</ul>
</div>
<div class="top-bar-right">
<ul class="menu">
<li><a href="/login/">Sign in</a></li>
<li><button class="button small" data-toggle="offCanvas">Menu</button></li>
</ul>
</div>
</div>$(document).foundation();
// Foundation's responsive toggle API: change behaviour at a breakpoint
const breakpoint = Foundation.MediaQuery.current; // 'small' | 'medium' | 'large' | ...
Foundation.MediaQuery.atLeast('medium'); // boolean
// Re-initialise plugins for markup injected after load
const $newRegion = $(htmlString);
$('#content').append($newRegion);
$newRegion.foundation();
$newRegion.find('[data-toggler]').each(function () {
Foundation.Toggler.reflow($(this)); // rebuild state, do not re-construct
});| Pattern | Requires | Mobile behaviour |
|---|---|---|
menu | Nothing | Horizontal list that wraps unless made vertical |
dropdown menu | data-dropdown-menu + foundation.dropdownMenu | Click to open at small sizes |
top-bar | foundation.topbar | Stacks the left and right regions |
equalizer on the menu | foundation.equalizer | Keeps sibling heights aligned |
⚠️
Dropdown menus open on click for touch and hover for pointer devices, decided by What-Input's
data-whatinput value. If you remove the What-Input script, dropdowns on hybrid laptops stop behaving predictably — it is a real dependency, not a nicety.Off-canvas panels
<div class="off-canvas-wrapper">
<div class="off-canvas-wrapper-inner" data-off-canvas-wrapper>
<div class="off-canvas position-left" id="offCanvas" data-off-canvas data-transition="overlap">
<ul class="vertical menu" data-accordion-menu>
<li><a href="/dashboard/">Dashboard</a></li>
<li>
<a href="#">Reports</a>
<ul class="menu vertical nested">
<li><a href="/reports/monthly/">Monthly</a></li>
<li><a href="/reports/annual/">Annual</a></li>
</ul>
</li>
</ul>
</div>
<div class="off-canvas-content" data-off-canvas-content>
<div class="top-bar">
<div class="top-bar-left">
<button class="button" type="button" data-toggle="offCanvas">Menu</button>
</div>
</div>
<main class="grid-container">
<h1 class="h3">Dashboard</h1>
</main>
</div>
</div>
</div>// Reach the instance to drive it from code
const offCanvas = new Foundation.OffCanvas($('#offCanvas'), {
transition: 'overlap',
forceTo: 'left',
closeOnClick: true,
trapFocus: true // keep Tab inside the panel while it is open
});
// Close it when a link inside is followed
$('#offCanvas').on('click', 'a[href]', () => {
Foundation.OffCanvas.getInstance($('#offCanvas')).close();
});
// The events bubble, so one listener covers many panels
$('[data-off-canvas]').on('opened.zf.offcanvas', (event) => {
console.log('opened', event.target.id);
});data-off-canvas-wrapperanddata-off-canvas-contentare structural: without them the panel is positioned against the wrong box and slides in from nowhere.data-transition="overlap"slides the panel over the content;pushmoves the content;revealslides both. Push and reveal need the wrapper or the page will scroll horizontally.trapFocus: trueis what makes the panel usable from a keyboard — the default focus handling does not confine Tab.forceTopins the panel to one edge instead of letting Foundation choose by screen size.
Breadcrumbs, pagination and accordion menus
<nav aria-label="You are here">
<ul class="breadcrumbs">
<li><a href="/">Home</a></li>
<li><a href="/reports/">Reports</a></li>
<li class="disabled">September</li>
<li class="current"><span>Summary</span></li>
</ul>
</nav>
<ul class="pagination" role="navigation" aria-label="Pagination">
<li class="pagination-previous disabled">Previous</li>
<li class="current"><span class="show-for-sr">You are on page</span>1</li>
<li><a href="?p=2" aria-label="Page 2">2</a></li>
<li><a href="?p=3" aria-label="Page 3">3</a></li>
<li class="ellipsis" aria-hidden="true"></li>
<li><a href="?p=9" aria-label="Page 9">9</a></li>
<li class="pagination-next"><a href="?p=2" aria-label="Next page">Next</a></li>
</ul>
<ul class="accordion-menu" data-accordion-menu>
<li><a href="#">Getting started</a>
<ul class="menu vertical nested">
<li><a href="/docs/install/">Install</a></li>
<li><a href="/docs/build/">Build</a></li>
</ul>
</li>
</ul>| Component | Accessible addition | Why |
|---|---|---|
| Breadcrumbs | .disabled, .current, aria-label | Marks the trail's end and its name |
| Pagination | show-for-sr text plus aria-label on links | Numbered links alone say nothing out of context |
| Accordion menu | data-accordion-menu | Adds aria-expanded to the parent link |
| Top bar | Nothing — roles are yours to add | The framework does not add navigation landmarks |
Once you go past two levels of navigation, a menu is the wrong control. Foundation's accordion menu will happily nest forever, but nesting beyond one level is a sign the information architecture needs work, not more markup.
FAQ
Why does my off-canvas panel not slide in?
In order of frequency: the panel is outside
.off-canvas-wrapper, the content region is missing data-off-canvas-content, or the trigger's data-toggle value does not match the panel's id. Use data-open="panelId" on a button to avoid the id mismatch entirely.How do I make the top bar sticky?
Add
data-sticky-container around the bar and a sticky element inside it, then initialise $(document).foundation(). Remember that a sticky element inside an ancestor with overflow: hidden will not stick.Related
Interactive plugins: reveals, tabs, accordions and dropdowns Building a complete page with Foundation
Last refreshed 2026-09-18.