Slide Toggle Content on Click in WordPress

A slide toggle in WordPress shows or hides a block of content when a visitor clicks a button: an FAQ answer, a “Read more” section, a filter panel. The classic way is jQuery’s slideToggle(), but block themes no longer load jQuery on the front end, so the old snippet stops with “jQuery is not defined”.

This version does the same slide in plain JavaScript with the Web Animations API. It is keyboard accessible, tells screen readers whether the content is open, and respects the reduced motion setting.

The markup

Give the clickable element the class trigger and the content the class toggled-object. In the block editor, use Advanced → Additional CSS class(es) on a Heading or Paragraph block for the trigger and on a Group block for the content; the script makes the trigger keyboard accessible. In hand-written HTML, use a <button>:

<button type="button" class="trigger">Show details</button>

<div class="toggled-object">
	<p>Hidden content that slides open.</p>
</div>

Several toggles on one page: give each content block an HTML anchor (for example faq-1) and add data-target="#faq-1" to its trigger. Without data-target, a trigger toggles every .toggled-object, as the old jQuery version did.

The CSS

/* The toggled content starts closed. */
.toggled-object {
	display: none;
}

The JavaScript

(function () {
	var DURATION = 300; // Animation length in milliseconds.
	var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

	function targetsFor(trigger) {
		// data-target="#faq-1" toggles one element; without it, every .toggled-object.
		return document.querySelectorAll(trigger.getAttribute('data-target') || '.toggled-object');
	}

	function isOpen(el) {
		if (el.dpcOpen === undefined) {
			el.dpcOpen = !el.hidden && window.getComputedStyle(el).display !== 'none';
		}
		return el.dpcOpen;
	}

	function animate(el, from, to, done) {
		if (el.dpcSlide) {
			el.dpcSlide.cancel();
		}
		if (reduceMotion || !el.animate) {
			if (done) {
				done();
			}
			return;
		}
		el.style.overflow = 'hidden';
		el.dpcSlide = el.animate([{ height: from + 'px' }, { height: to + 'px' }], { duration: DURATION, easing: 'ease' });
		el.dpcSlide.onfinish = function () {
			el.style.overflow = '';
			el.dpcSlide = null;
			if (done) {
				done();
			}
		};
	}

	function open(el) {
		var from = el.dpcSlide ? el.offsetHeight : 0;
		el.dpcOpen = true;
		el.hidden = false;
		el.style.removeProperty('display');
		if (window.getComputedStyle(el).display === 'none') {
			el.style.display = 'block';
		}
		animate(el, from, el.scrollHeight);
	}

	function close(el) {
		el.dpcOpen = false;
		animate(el, el.offsetHeight, 0, function () {
			el.style.display = 'none';
		});
	}

	function toggle(trigger) {
		var targets = targetsFor(trigger);
		var opened = false;
		targets.forEach(function (el) {
			if (isOpen(el)) {
				close(el);
			} else {
				open(el);
				opened = true;
			}
		});
		trigger.setAttribute('aria-expanded', opened ? 'true' : 'false');
	}

	document.querySelectorAll('.trigger').forEach(function (trigger) {
		var targets = targetsFor(trigger);
		if (!targets.length) {
			return;
		}
		var native = trigger.tagName === 'BUTTON';
		if (!native) {
			trigger.setAttribute('role', 'button');
			trigger.tabIndex = 0;
		}
		trigger.setAttribute('aria-expanded', isOpen(targets[0]) ? 'true' : 'false');
		if (targets[0].id) {
			trigger.setAttribute('aria-controls', targets[0].id);
		}

		trigger.addEventListener('click', function (event) {
			event.preventDefault();
			toggle(trigger);
		});
		if (!native) {
			trigger.addEventListener('keydown', function (event) {
				if (event.key === 'Enter' || event.key === ' ') {
					event.preventDefault();
					toggle(trigger);
				}
			});
		}
	});
})();

Add it with Scripts Organizer

In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Conditions and set Template to All (or only the pages that use the toggle). Under Script location tick Header and Footer. Put the CSS in the header with Header Mode CSS, and the JavaScript in the footer with Footer Mode Javascript, then publish.

Scripts Organizer prints header and footer code on the front end only with the Conditions trigger, not with Everywhere. The footer runs after the page content, so no document-ready wrapper is needed.

How it works

  • On click the script checks whether the content is open. Opening shows it and animates its height from 0 to its full scrollHeight; closing animates to 0 and then sets display: none.
  • el.animate() runs the animation in the browser without jQuery. Clicking again during a slide cancels it and slides back from the current height.
  • The trigger gets aria-expanded and, when the content has an id, aria-controls. A trigger that is not a <button> also gets role="button", focus with Tab and Enter/Space support.
  • With reduced motion turned on, the content opens and closes without animating.

Customise it

  • Speed: change DURATION.
  • Start open: delete the CSS. The first click then closes the content.
  • Other class names: change .trigger in querySelectorAll() and .toggled-object in targetsFor() and the CSS.

Variant: jQuery

If your site already loads jQuery and you prefer it, this is the old snippet in a safe wrapper. On a block theme, load jQuery first with wp_enqueue_script( 'jquery' ). See How to start with jQuery in WordPress.

jQuery(function ($) {
	$('.trigger').on('click', function () {
		$('.toggled-object').slideToggle(300);
	});
});

Gotchas

  • Padding on the toggled element itself stays visible during the slide. Put the padding on an inner Group instead.
  • Content hidden with CSS is also hidden when JavaScript fails. For content that must always be reachable, consider the Details block, which opens and closes without any script.

In the Scripts Organizer library Import it with one click from Scripts Organizer.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You’ll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You'll also get DPlugins product updates. No spam, and you can unsubscribe anytime.