Interactive plugins: reveals, tabs, accordions and dropdowns
Initialise Foundation's JavaScript components correctly, drive them from data attributes or the API, listen to their events, and tear them down cleanly.
Two ways to initialise
$(document).foundation(); // 1. sweeping init of the whole document
$(document).foundation({
reveal: { animationIn: 'fade-in', animationOut: 'fade-out' },
tabs: { matchHeight: true },
accordion: { multiExpand: true, allowAllClosed: true }
}); // 2. the same, with option overrides
// 3. a single component, with its own instance
const modal = new Foundation.Reveal($('#dialog'), {
animationIn: 'slide-in-down',
animationOut: 'slide-out-up',
closeOnEsc: true,
multipleOpened: false
});
modal.open();| Plugin | Data attribute | Extra requirement |
|---|---|---|
| Reveal (modal) | data-reveal | data-open="id" or data-toggle on the trigger |
| Tabs | data-tabs on the tab container | data-tabs-content on the panel wrapper is optional |
| Accordion | data-accordion | data-accordion-item per entry if not using the class |
| Dropdown | data-dropdown | Menu and content elements linked by aria-controls or id |
| Tooltip | data-tooltip | title or data-tooltip text |
| Orbit (carousel) | data-orbit | Images or slides inside a ul |
💡
The sweeping
$(document).foundation() call is convenient but blunt: it initialises every plugin it finds, including the ones you never use. On a large page prefer explicit construction for the components that carry options, and let the sweep handle the trivial ones.Tabs, accordions and reveals
<ul class="tabs" data-tabs id="account-tabs">
<li class="tabs-title is-active"><a href="#profile" aria-selected="true">Profile</a></li>
<li class="tabs-title"><a href="#billing">Billing</a></li>
</ul>
<div class="tabs-content" data-tabs-content="account-tabs">
<div class="tabs-panel is-active" id="profile"><p>Profile settings.</p></div>
<div class="tabs-panel" id="billing"><p>Billing settings.</p></div>
</div>
<ul class="accordion" data-accordion data-multi-expand="false" data-allow-all-closed="true">
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Shipping</a>
<div class="accordion-content" data-tab-content>
<p>Orders ship within two business days.</p>
</div>
</li>
<li class="accordion-item" data-accordion-item>
<a href="#" class="accordion-title">Returns</a>
<div class="accordion-content" data-tab-content>
<p>Thirty days, unopened.</p>
</div>
</li>
</ul>- The tab container and the panel wrapper are linked by the
data-tabs-contentvalue matching the tabid. Get the link wrong and clicking a tab changes nothing. - The active tab carries
aria-selected="true"in the markup; the plugin updates it afterwards, but starting from the correct value avoids a wrong announcement on load. - An accordion item is a
liwith a title link and a content element. Withoutdata-tab-contenton the panel, the height animation cannot measure it. data-allow-all-closedmatters more than it looks: without it the user cannot collapse the last open panel.
Events and teardown
// Foundation events are namespaced .zf.<plugin> and bubble from the element.
$('#dialog').on('open.zf.reveal', () => {
// about to open: veto with return false
});
$('#dialog').on('opened.zf.reveal', () => {
console.log('visible now');
});
$('#account-tabs').on('change.zf.tabs', (event, $target) => {
console.log('activated panel', $target.attr('id'));
});
$('.accordion').on('down.zf.accordion', (event, $target) => {
console.log('opening', $target.find('.accordion-title').text());
});
// Because they bubble, one delegated listener can observe every component of a kind:
$(document).on('opened.zf.offcanvas', (event) => console.log('offcanvas open:', event.target.id));// Teardown: destroy the instance before replacing its element.
const $panel = $('#legacyPanel');
const instance = Foundation.Tabs.getInstance($panel);
instance?.destroy();
$panel.replaceWith(newMarkup);
// After inserting new markup, initialise only that subtree — not the whole document.
const $region = $('#main');
$region.html(newMarkup);
$region.foundation();
// Removing an element without destroying its plugin leaves jQuery-bound
// handlers and a Foundation instance keyed to a detached node.
$('#dialog').on('closed.zf.reveal', function () {
Foundation.Reveal.getInstance($(this))?.destroy();
$(this).remove();
});| Task | Call | Note |
|---|---|---|
| Get the instance | Foundation.Reveal.getInstance($el) | Returns a live instance or nothing |
| Destroy it | instance.destroy() | Unbinds handlers and clears generated ids |
| Re-initialise a subtree | $subtree.foundation() | Preferred over a document-wide sweep |
| Rebuild state without rebuilding | Foundation.Toggler.reflow($el) | For components with dynamic content |
FAQ
Why do my components stop working after an AJAX page load?
The new markup was never initialised. Call
$newContent.foundation() after insertion. If the content replaces a region that already had instances, destroy those first so you do not end up with two sets of handlers on the same node.Do Foundation events use the same naming as Bootstrap?
No. Foundation uses
<action>.zf.<plugin> — open.zf.reveal, change.zf.tabs — and passes values as extra arguments to the handler rather than on the event object. Code ported from Bootstrap will silently never fire.Related
Building navigation Motion UI and animation
Last refreshed 2026-09-18.