Collapsify example
How it works
Show and hide content: a toggle, an accordion or a set of tabs, all from the same package and the same markup.
Collapsify opens and closes a block of content when a control is clicked. The content slides from zero to its height and back, and the control and the content get an active modifier and the right aria-* values. One option turns the same thing into an accordion (only one block open at a time) and another into tabs (one block always open). It runs on the @terrahq/collapsify package, written in vanilla JS.
Controls and contents are matched by an id in a data-* attribute, not by their position in the HTML, so a control can live anywhere on the page and several controls can open the same content. The class in src/js/handler/collapsify maps each selector to its options and adds the callbacks the framework needs.
The markup
A control with data-collapsify-control="id" and a content with data-collapsify-content="id". The content starts closed; give it a class ending in --is-active and it starts open. The aria-expanded, aria-controls and aria-hidden attributes are yours to write, and the package keeps them up to date.
<!-- A control and a content that share an id. The hook goes on any ancestor -->
<div class="js--collapsify">
<button class="c--btn-a" type="button" data-collapsify-control="content01" aria-expanded="false" aria-controls="basicContent01">Show hide</button>
<div id="basicContent01" data-collapsify-content="content01" aria-hidden="true">
<p>…</p>
</div>
</div> The handler
Every config shares two callbacks. Collapsify changes the height of the page, so when a block has finished opening or closing, every ScrollTrigger below it must recompute its start and end, or the animations further down fire at the wrong scroll position. updateScrollTriggers does that, skipping pinned triggers to avoid a jump:
this.callbacks = {
onComplete: () => {
// Update scroll triggers after collapsify is initialized
updateScrollTriggers({ Manager: this.Manager });
},
onSlideEnd: (isOpen, contentID) => {
// Update scroll triggers after collapsify slide ends
updateScrollTriggers({ Manager: this.Manager });
},
}; The simplest config is the element and the callbacks; the accordion and tabs configs add their options on top:
this.configSimple = ({ element }) => ({
element,
...this.callbacks,
}); What the framework does for you
- Lazy loading. An instance is only built when its element gets within 30px of the viewport (Boostify, scroll method). The package is imported once and cached by the Manager.
- Page transitions. Every instance is destroyed before swup replaces the content, whichever of the three selectors is on the page, and rebuilt on the new one.
- Height changes. Collapsify is registered with
modifyHeight: trueinresources.js. Any handler can fireCollapsify:loadthrough the EventSystem to build every instance at once, skipping the lazy load: AnchorTo does it for every height-modifying library before it scrolls, so an anchor below a closed accordion lands in the right place. Modal does the same for the libraries it opens.
Toggle
One button, one block. Click to open, click again to close.
The default behaviour: a single control that shows and hides a single content. The content slides over 400ms with an ease-in-out transition, and while it animates further clicks are ignored. When open, the button carries c--btn-a--is-active, built from its first class and the active suffix, so the button can show its state in SCSS.
<div class="js--collapsify">
<button class="c--btn-a" type="button" data-collapsify-control="content01" aria-expanded="false" aria-controls="basicContent01">Show hide Button</button>
<div id="basicContent01" data-collapsify-content="content01" aria-hidden="true">
<p>This content can be toggled open and closed…</p>
</div>
</div> this.configSimple = ({ element }) => ({
element,
...this.callbacks,
}); Accordion
Several blocks, one open at a time. Opening one closes the others.
closeOthers: truenameSpace: "accordion" closeOthers: true closes every other content of the instance when one opens, which is what makes it an accordion. A block can still be closed by clicking its own control, so the accordion can be fully collapsed. nameSpace: "accordion" renames the attributes to data-accordion-control and data-accordion-content: it keeps this instance apart from the toggle above, which otherwise would pick up its contents too, since the package matches attributes across the whole document.
All three blocks start closed. To start with one open, add the --is-active modifier to its content.
<!-- The namespace is "accordion", so the attributes are data-accordion-* -->
<div class="c--accordion-a js--collapsify-accordion">
<button class="c--btn-a" type="button" data-accordion-control="simpleContent-01" aria-expanded="false" aria-controls="accordionContent-01">Content 01</button>
<div id="accordionContent-01" data-accordion-content="simpleContent-01" aria-hidden="true">
<p>…</p>
</div>
<button class="c--btn-a" type="button" data-accordion-control="simpleContent-02" aria-expanded="false" aria-controls="accordionContent-02">Content 02</button>
<div id="accordionContent-02" data-accordion-content="simpleContent-02" aria-hidden="true">
<p>…</p>
</div>
…
</div> this.configAccordion = ({ element }) => ({
element,
closeOthers: true,
nameSpace: "accordion",
...this.callbacks,
}); closeOthers: truenameSpace: "accordion" Tabs
One block always open. A row of buttons on desktop, a dropdown for small screens.
isTab: truenameSpace: "tab"dropdownElement isTab: true changes one rule: clicking the control of the open content does nothing, so there is always exactly one tab showing. The first content starts open thanks to its c--tabs-a__bd--is-active modifier; without it every tab would start closed. nameSpace: "tab" gives the attributes their own prefix, as in the accordion.
dropdownElement is a <select> that drives the same tabs, meant for narrow screens where a row of buttons does not fit. Each option names its tab with data-tab-dropdown-item; on load the option of the open tab is selected, and choosing another one switches tab. The handler finds the select inside the element with element.querySelector(".c--tabs-a__hd__wrapper__item").
<!-- The namespace is "tab". The content with the --is-active modifier starts open -->
<div class="c--tabs-a js--collapsify-tab">
<div class="c--tabs-a__hd">
<ul class="c--tabs-a__hd__list-group">
<li class="c--tabs-a__hd__list-group__list-item">
<button class="c--tabs-a__hd__list-group__list-item__link c--btn-a" type="button" data-tab-control="tab-01" aria-expanded="true">Tab 01</button>
</li>
<li class="c--tabs-a__hd__list-group__list-item">
<button class="c--tabs-a__hd__list-group__list-item__link c--btn-a" type="button" data-tab-control="tab-02" aria-expanded="false">Tab 02</button>
</li>
…
</ul>
<!-- The dropdown: the handler passes it as dropdownElement -->
<div class="c--tabs-a__hd__wrapper">
<select aria-label="tab selector" class="c--tabs-a__hd__wrapper__item">
<option value="" disabled selected>Select</option>
<option data-tab-dropdown-item="tab-01" value="">option 01</option>
<option data-tab-dropdown-item="tab-02" value="">option 02</option>
…
</select>
</div>
</div>
<div class="c--tabs-a__bd c--tabs-a__bd--is-active" data-tab-content="tab-01" aria-hidden="false">…</div>
<div class="c--tabs-a__bd" data-tab-content="tab-02" aria-hidden="true">…</div>
…
</div> this.configTabs = ({ element }) => ({
element,
isTab: true,
nameSpace: "tab",
dropdownElement: element.querySelector(".c--tabs-a__hd__wrapper__item"),
...this.callbacks
}); isTab: truenameSpace: "tab"dropdownElement Content First: Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolores nostrum amet excepturi eum. Quo labore, est inventore incidunt debitis voluptatum qui itaque iste quam, asperiores aliquid illum optio atque quidem.
Options reference
Everything you can set, in the config of Handler.js, and the attributes the package reads from the HTML.
Config (Handler.js)
| Option | Default | What it does |
|---|---|---|
element | required | The element the handler found with the selector. Used to find the dropdown; the controls and contents are matched by attribute across the document |
nameSpace | "collapsify" | Prefix of the attributes: data-<nameSpace>-control, -content and -dropdown-item. Use a different one per instance on the same page |
closeOthers | true | Opening a content closes the others. false lets several stay open |
isTab | false | Tabs mode: the control of the open content does nothing, so one is always open |
dropdownElement | none | A <select> whose options carry data-<nameSpace>-dropdown-item="id". Changing it opens that content |
isAnimation | true | Slides the content open and closed. false switches instantly |
animationSpeed | 400 | Duration of the slide in ms |
cssEasing | "ease-in-out" | CSS transition easing of the slide |
activeClass | "--is-active" | Suffix added to the first class of the open control and content, e.g. c--btn-a--is-active |
onSlideStart | none | (isOpen, contentID) => {}, called when a content starts opening or closing |
onSlideEnd | none | (isOpen, contentID) => {}, called when the slide has finished. The handler refreshes the ScrollTriggers here |
onComplete | none | () => {}, called once after the setup. The handler refreshes the ScrollTriggers here too |
Attributes (HTML)
| Attribute | Where | What it does |
|---|---|---|
data-collapsify-control="id" | Any clickable element | Opens and closes the content with that id. Several controls can share an id |
data-collapsify-content="id" | The block to show and hide | One per id. Starts open when one of its classes ends in --is-active |
data-collapsify-dropdown-item="id" | An <option> of dropdownElement | Selecting it opens the content with that id |
aria-expanded, aria-controls, aria-hidden | Control and content | Written by you, kept in sync by the package |
With another nameSpace the prefix changes: data-accordion-* and data-tab-* on this page. The instance also exposes open(id) and close(id).