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();
PluginData attributeExtra requirement
Reveal (modal)data-revealdata-open="id" or data-toggle on the trigger
Tabsdata-tabs on the tab containerdata-tabs-content on the panel wrapper is optional
Accordiondata-accordiondata-accordion-item per entry if not using the class
Dropdowndata-dropdownMenu and content elements linked by aria-controls or id
Tooltipdata-tooltiptitle or data-tooltip text
Orbit (carousel)data-orbitImages 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-content value matching the tab id. 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 li with a title link and a content element. Without data-tab-content on the panel, the height animation cannot measure it.
  • data-allow-all-closed matters 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();
});
TaskCallNote
Get the instanceFoundation.Reveal.getInstance($el)Returns a live instance or nothing
Destroy itinstance.destroy()Unbinds handlers and clears generated ids
Re-initialise a subtree$subtree.foundation()Preferred over a document-wide sweep
Rebuild state without rebuildingFoundation.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.

Building navigation Motion UI and animation

Last refreshed 2026-09-18.