Collapse
VanillaJS collapse with transition in 60 lines of code
by Arnold Daniels
HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
<section>
<details>
<summary>Lorem Ipsum</summary>
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
</details>
</section>
<section>
<aside class="menu">
<p class="menu-label">
General
</p>
<ul class="menu-list">
<li><a>Dashboard</a></li>
<li><a>Customers</a></li>
</ul>
<p class="menu-label">
Administration
</p>
<ul class="menu-list collapse">
<li><a>Team Settings</a></li>
<li>
<a class="is-active">Manage Your Team</a>
<ul>
<li><a>Members</a></li>
<li>
<a>Plugins</a>
<ul>
<li><a>Payments</a></li>
<li><a>Transfers</a></li>
<li><a>Balance</a></li>
</ul>
</li>
<li><a>Add a member</a></li>
</ul>
</li>
<li><a>Invitations</a></li>
<li><a>Cloud Storage Environment Settings</a></li>
<li><a>Authentication</a></li>
</ul>
<p class="menu-label">
Transactions
</p>
<ul class="menu-list">
<li><a>Payments</a></li>
<li><a>Transfers</a></li>
<li><a>Balance</a></li>
</ul>
</aside>
</section>
<section>
<button type="button" class="button" id="lorem-button">Lorem Ipsum</button>
<div id="lorem" class="collapse">
<p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.</p>
</div>
</section>
SCSS
details {
transition: height 0.5s;
overflow: hidden;
}
summary {
cursor: pointer;
&:focus {
outline: none;
}
}
ul.collapse {
li {
transition: all 0.4s;
overflow: hidden;
}
li:not([data-open]) > ul {
display: none;
}
}
div.collapse {
transition: height 0.2s;
overflow: hidden;
&:not([data-open]) {
display: none;
}
}
/* Spacing, has nothing to do with collapse */
body {
padding: 20px;
}
section+section {
margin-top: 40px;
}
JavaScript
function delegateEvent(type, selector, handler) {
document.addEventListener(type, function(event) {
for (var target = event.target; target && target != this; target = target.parentNode) {
if (target.matches(selector)) {
handler.call(target, event);
break;
}
}
}, false);
}
function toggleCollapse(details, summary, event) {
if (details.style.height !== '') {
if (event !== undefined) event.preventDefault();
return; // Still in transition
}
const isDetailsNode = details.tagName.toLowerCase() === 'details';
const isOpen = isDetailsNode ? details.open : details.dataset.open !== undefined;
if (!hasTransition(details, 'height')) {
if (isDetailsNode) return; // <details> does this natively
// Simple toggle.
if (!isOpen) details.dataset.open = '';
if (isOpen) delete details.dataset.open;
return;
}
details.style.height = details.offsetHeight + "px";
if (isDetailsNode && isOpen && event !== undefined) event.preventDefault(); // Don't hide during transition
if (!isDetailsNode && !isOpen) details.dataset.open = '';
setTimeout(function() {
const toHeight = isOpen
? (summary !== undefined ? summary.offsetHeight : 0)
: details.scrollHeight;
if (details.style.height === toHeight + "px") { // No transition
details.dispatchEvent(new Event('transitionend'));
return;
}
details.style.height = toHeight + "px";
});
details.addEventListener('transitionend', function() {
if (isDetailsNode && isOpen) details.open = false;
if (!isDetailsNode && isOpen) delete details.dataset.open;
details.style.height = '';
}, {once: true});
}
function hasTransition(element, property) {
const transitions = getComputedStyle(element).transition.split(/\s*,\s*/);
const transition = transitions.find((trans) => [property, 'all'].includes(trans.split(/\s+/)[0]));
return transition !== null &&...