Dropdown Simple

Dropdown
GSAP
JS
JS Library
Last Updated: Feb 1, 2026
  • Set data-preview="true" on .dropdown-1_wrap to preview the dropdown as open in designer
  • Set the aria-label="Dropdown Text" on the .dropdown-1_toggle_clickable to match the text of the dropdown
  • Set data-open-on-hover-in="true" on the .dropdown-1_wrap to open the dropdown on hover in
  • Set data-close-on-hover-out="true" on the .dropdown-1_wrap to close the dropdown on hover out
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
	document.querySelectorAll(".dropdown-1_wrap").forEach((component, index) => {
		if (component.hasAttribute("data-dropdown-1")) return;
		component.setAttribute("data-dropdown-1", "");

		const button  = component.querySelector(".dropdown-1_toggle_clickable");
		const content = component.querySelector(".dropdown-1_content");
		const toggle  = component.querySelector(".dropdown-1_toggle_wrap");
		const helper  = component.querySelector(".dropdown-1_helper");

		button.setAttribute("id", `dropdown-btn-${index}`);
		button.setAttribute("aria-controls", `dropdown-${index}`);
		content.setAttribute("id", `dropdown-${index}`);
		content.setAttribute("aria-labelledby", `dropdown-btn-${index}`);

		const isTouch = window.matchMedia("(hover: none) and (pointer: coarse)").matches;
	  if (helper && !isTouch) {
			function sizeHelper() {
				const h = toggle.offsetHeight;
				helper.style.height = h + "px";
			}
			sizeHelper();
			new ResizeObserver(sizeHelper).observe(toggle);

			function updateClipPath(xPercent, yPercent) {

				helper.style.clipPath = `polygon(${xPercent}% ${yPercent}%, 100% 100%, 0% 100%)`;
			}
			updateClipPath(50, 0);

			component.addEventListener("mousemove", (e) => {
				const toggleRect = toggle.getBoundingClientRect();
				const helperRect = helper.getBoundingClientRect();
				if (
					e.clientX >= toggleRect.left &&
					e.clientX <= toggleRect.right &&
					e.clientY >= toggleRect.top &&
					e.clientY <= toggleRect.bottom
				) {
					const x = e.clientX - helperRect.left;
					const y = e.clientY - toggleRect.top;
					const xPct = Math.round((x / helperRect.width) * 100);
					const yPct = Math.round((y / toggleRect.height) * 100);
					updateClipPath(xPct, yPct);
				}
			});
		}

		function openDropdown() {
			button.setAttribute("aria-expanded", "true");
			component.classList.add("is-active");
			let tl = gsap.timeline({
				onComplete: () => { if (typeof ScrollTrigger !== "undefined") ScrollTrigger.refresh(); }
			});
			tl.set(content, { display: "block" });
			tl.fromTo(content, { height: 0 }, { height: "auto", ease: "power1.inOut", duration: 0.4 });
		}

		function closeDropdown() {
			button.setAttribute("aria-expanded", "false");
			component.classList.remove("is-active");
			let tl = gsap.timeline({
				onComplete: () => { if (typeof ScrollTrigger !== "undefined") ScrollTrigger.refresh(); }
			});
			tl.to(content, { height: 0, ease: "power1.inOut", duration: 0.4 });
			tl.set(content, { display: "none" });
		}

		function isOpen() {
			return button.getAttribute("aria-expanded") === "true";
		}

		button.addEventListener("click", () => {
			isOpen() ? closeDropdown() : openDropdown();
		});

		document.addEventListener("keydown", function (e) {
			if (e.key === "Escape" && isOpen()) {
				closeDropdown();
				button.focus();
			}
			if ((e.key === "ArrowDown" || e.key === "ArrowUp") && isOpen() && component.contains(document.activeElement)) {
				e.preventDefault();
				const items = [...content.querySelectorAll("a, button")];
				if (!items.length) return;
				const currentIndex = items.indexOf(document.activeElement);
				let nextIndex = currentIndex === -1
					? (e.key === "ArrowDown" ? 0 : items.length - 1)
					: (currentIndex + (e.key === "ArrowDown" ? 1 : -1) + items.length) % items.length;
				items[nextIndex].focus();
			}
		});

		document.addEventListener("click", (e) => {
			if (isOpen() && !component.contains(e.target)) closeDropdown();
		});

		if (component.getAttribute("data-open-on-hover-in") === "true") {
			component.addEventListener("mouseenter", () => { if (!isOpen()) openDropdown(); });
		}
		if (component.getAttribute("data-close-on-hover-out") === "true") {
			component.addEventListener("mouseleave", () => { if (isOpen()) closeDropdown(); });
		}
	});
});
</script>