<h1>Accordion with <code>&lt;details&gt;</code> element</h1>
<p>Making the native <abbr>HTML</abbr> <code>&lt;details&gt;</code> element behave like a typical collapsible accordion with smooth animations.</p>

<p><input type="checkbox" id="accordion-toggle"><label for="accordion-toggle">Keep animations but remove auto-collapse effect </label></p>

<div class="container collapse">
  <details>
    <summary>Accordion heading 1</summary>
    <div class="details-wrapper">
      <div class="details-styling">        
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsa quis fuga saepe sunt delectus, quo assumenda dolorum officiis odit optio modi, aspernatur necessitatibus libero, itaque repellendus. Incidunt blanditiis magni velit.</p>
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsa quis fuga saepe sunt delectus, quo assumenda dolorum officiis odit optio modi, aspernatur necessitatibus libero, itaque repellendus. Incidunt blanditiis magni velit.</p>
      </div>
    </div>
  </details>
  <details>
    <summary>Accordion heading 2</summary>
    <div class="details-wrapper">
      <div class="details-styling">        
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsa quis fuga saepe sunt delectus, quo assumenda dolorum officiis odit optio modi, aspernatur necessitatibus libero, itaque repellendus. Incidunt blanditiis magni velit.</p>
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsa quis fuga saepe sunt delectus, quo assumenda dolorum officiis odit optio modi, aspernatur necessitatibus libero, itaque repellendus. Incidunt blanditiis magni velit.</p>
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsa quis fuga saepe sunt delectus, quo assumenda dolorum officiis odit optio modi, aspernatur necessitatibus libero, itaque repellendus. Incidunt blanditiis magni velit.</p>
      </div>
    </div>
  </details>
  <details>
    <summary>Accordion heading 3</summary>
    <div class="details-wrapper">   
      <div class="details-styling">        
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsa quis fuga saepe sunt delectus, quo assumenda dolorum officiis odit optio modi, aspernatur necessitatibus libero, itaque repellendus. Incidunt blanditiis magni velit.</p>
      </div>
    </div>
  </details>
</div>

<p>Using the <a href="https://github.com/javan/details-element-polyfill">details-element-polyfill</a> for IE/Edge support</p>
// Classes set via JS
$accordionClass: '.collapse-init';
$panelClass: '.panel-active';
$contentsClass: 'summary + *';

/*
  Please wrap your collapsible content in a single element or so help me
  Must add a transition or it breaks because that's the whole purpose of this
  Only one transition-duration works (see explanation on line #141 in JS)
  You can add more to an inner wrapper (.details-styling)
*/
//Simplified: .details-wrapper{}
#{$accordionClass $contentsClass} {
  transition: all 0.25s ease-in-out;
  overflow: hidden; // because we're animating height
}

/*
  Closed state. Any CSS transitions work here
  The JS has a height calculation to make sliding opened/closed easier, but it's not necessary
  Remove the height prop for a simple toggle on/off (after all that work I did for you?)
*/
//Simplified: :not(.panel-active) .details-wrapper {}
#{$accordionClass} :not(#{$panelClass}) #{$contentsClass} {  
  height: 0;
  opacity: 0;
  transform: scale(0.9);
  transform-origin: bottom center;
}

// Let's get rid of the default arrows so we can style our own, as we must find whatever little joy we can in this garbage web
#{$accordionClass} {
  summary { list-style: none; } // Spec
  summary::-webkit-details-marker { display: none; } // Chrome
  summary::before { display: none; } // Polyfill
    
  // Should we do this? No idea
  summary { cursor: pointer; }
}

/*
  This element exists so .details-wrapper has no extra margin/padding that can screw with the smooth height collapse
  You can style .details-wrapper decoratively but avoid anything that modifies the box and add it to .details-styling instead
  Otherwise, this element is totally optional. Remove if you hate divitis
*/
.details-styling {
  padding: 1em;
}

//======= Non-essential page styling, ignore
$hue: 260;

$background: hsl($hue, 90, 98);
$text: hsl($hue, 20, 25);

$primary: hsl($hue, 85, 50);
$link: hsl($hue, 75, 65);
$border: hsl($hue, 20, 85);

::selection {
  $rot: 140;
  
  background: hsl($hue + $rot, 95, 70);
  color: adjust-hue($text, $rot);
}

html {
  background: $background;
  color: $text;
}

body {
  font: 1em/1.4 'Quicksand', sans-serif;
  margin: 0 auto;
  max-width: 960px;
  padding: 5vw;
}

h1 {
  font-size: 2em;
  margin-bottom: 1em;
  text-align: center;
  
  + p {
    margin-left: auto;
    margin-right: auto;
    max-width: 50ch;
  }
  
  ~ p {
    font-size: 1.2em;
    text-align: center;
  }
}

p {
  margin-top: 0;
  margin-bottom: 1em;
  
  &:last-child { margin-bottom: 0; }
}

code {
  $rot: 90;
  
  background: hsla($hue + $rot, 70, 70, 0.1);
  color: hsl($hue + $rot - 5, 75, 65);
}

a {
  color: $link;
  box-shadow: inset 0 -3px lighten($link, 20);
  font-weight: 700;
  text-decoration: none;
  transition: 0.2s;
  
  &:hover,
  &:focus {
    box-shadow: inset 0 -1.2em $link;
    color: $background;
  }
}

abbr {
  font-variant: small-caps;
  text-transform: lowercase;
  font-size: 1.2em;
}

[type=checkbox] {
  opacity: 0;
  position: absolute;
  width: 0;
  height: 0;
  
  + label {
    background: lighten($primary, 45);
    border-left: 4px solid $primary;
    cursor: pointer;
    display: block;
    font-size: 1rem;
    font-weight: 700;
    text-align: left;
    transition: 0.1s;
    padding: 0.75em 1em;
    
    &::before {
      border: 2px solid;
      border-radius: 2px;
      color: $primary;
      content: '';
      display: inline-block;
      margin-right: 0.75ch;
      transition: 0.1s;
      width: 1ch;
      height: 1ch;
      vertical-align: baseline;
    }
  }
  
  &:focus + label {
    outline: 2px solid $primary;    
  }
  
  &:checked + label::before {
    background: currentColor;
    box-shadow: inset 0 0 0 2px #fff;
  }
}

.container {
  box-shadow: 0.2em 1em 2em -1em $border;
  margin: 2.4em 0;
}

//==== Accordion element styling
details {
  $b: 6px;
  
  background: #fff;
  border: 1px solid $border;
  border-bottom: 0;
  list-style: none;
  
  &:first-child {
    border-radius: $b $b 0 0;
  }
  
  &:last-child {
    border-bottom: 1px solid $border;
    border-radius: 0 0 $b $b;
  }
}

summary {
  $arrow-size: 0.5em;
  
  display: block;
  transition: 0.2s;
  font-weight: 700;
  padding: 1em;
    
  &:focus {
    outline: 2px solid $primary;
  }
  
  #{$accordionClass} &::after {
    border-right: 2px solid;
    border-bottom: 2px solid;
    content: '';
    float: right;
    width: $arrow-size;
    height: $arrow-size;
    margin-top: $arrow-size/2;
    transform: rotate(45deg);
    transition: inherit;
  }
  
  [open] & {
    background: $primary;
    color: $background;
    
    &::after { 
      margin-top: $arrow-size;
      transform: rotate(225deg);
    }
  }
}
View Compiled
miscPolyfillsForIE();

/*
let accordion = new Collapse(element, { option: value}).init();

  Options - { option: defaultValue }
    accordion: false,
    initClass: 'collapse-init',
    activeClass: 'panel-active',
    heightClass: 'collapse-reading-height',

  Methods - accordion.method(panel)
    open(panel)
    close(panel)
    toggle(panel)
    openSinglePanel(panel) [AKA accordion mode]
    openAll()
    closeAll()

  Events - panel.addEventListener('event')
    openingPanel
    openedPanel
    closingPanel
    closedPanel
*/

class Collapse {
  constructor(container, options = {}) {
    let defaults = {
      accordion: false,
      initClass: 'collapse-init',
      activeClass: 'panel-active',
      heightClass: 'collapse-reading-height',
    }
    
    this.settings = Object.assign({}, defaults, options);
    
    this._container = container;
    this._panels = container.querySelectorAll("details");
    
    this.events = {
      openingPanel: new CustomEvent('openingPanel'),
      openedPanel: new CustomEvent('openedPanel'),
      closingPanel: new CustomEvent('closingPanel'),
      closedPanel: new CustomEvent('closedPanel'),
    };
  }  

  // Sets height of panel content
  _setPanelHeight( panel ) {
    let contents = panel.querySelector("summary + *");
    
    contents.style.height = contents.scrollHeight + "px";
  }

  // Removes height of panel content
  _removePanelHeight( panel ) {
    let contents = panel.querySelector("summary + *");
    
    contents.style.height = null;
  }
  
  //=== Open panel
  open(panel) {
    panel.dispatchEvent( this.events.openingPanel );

    panel.open = true;
  }

  // Add height and active class, this triggers opening animation
  _afterOpen(panel) {
    this._setPanelHeight(panel);
    panel.classList.add(this.settings.activeClass);
  }

  // Remove height on animation end since it's no longer needed
  _endOpen(panel) {
    panel.dispatchEvent( this.events.openedPanel );
    
    this._removePanelHeight(panel);
  }
  
  //=== Close panel, not toggling the actual [open] attr!
  close(panel) {
    panel.dispatchEvent( this.events.closingPanel );
    this._afterClose(panel);
  }
  
  // Set height, wait a beat, then remove height to trigger closing animation
  _afterClose(panel) {
    this._setPanelHeight(panel);

    setTimeout(() => {
      panel.classList.remove(this.settings.activeClass);
      this._removePanelHeight(panel);
    }, 100); //help, this is buggy and hacky
  }

  // Actually closes panel once animation finishes
  _endClose(panel) {
    panel.dispatchEvent( this.events.closedPanel );
    
    panel.open = false;
  }
  
  //=== Toggles panel... just in case anyone needs this
  toggle(panel) {
    panel.open ? this.close(panel) : this.open(panel);
  }

  //=== Accordion closes all panels except the current passed panel 
  openSinglePanel(panel) {
    this._panels.forEach((element) => {
      if (panel == element && !panel.open) {
        this.open(element);
      } else {
        this.close(element);
      }
    });
  }
  
  //=== Opens all panels just because
  openAll() {
    this._panels.forEach((element) => {
      this.open(element);
    });
  }
  
  //=== Closes all panels just in case
  closeAll() {
    this._panels.forEach((element) => {
      this.close(element);
    });
  }
  
  // Now put it all together
  _attachEvents() {
    this._panels.forEach(panel => {
      let toggler = panel.querySelector("summary");
      let contents = panel.querySelector("summary + *");

      // On panel open
      panel.addEventListener("toggle", e => {
        let isReadingHeight = panel.classList.contains(this.settings.heightClass);

        if (panel.open && !isReadingHeight) {
          this._afterOpen(panel);
        }
      });

      toggler.addEventListener("click", e => {
        // If accordion, stop default toggle behavior
        if (this.settings.accordion) {
          this.openSinglePanel(panel);
          e.preventDefault();
        }
        
        // On attempting close, stop default close behavior to substitute our own
        else if (panel.open) {
          this.close(panel);
          e.preventDefault();
        }
        
        // On open, proceed as normal (see toggle listener above)
      });
      
      /*
        transitionend fires once for each animated property, 
        but we want it to fire once for each click. 
        So let's make sure to watch only a single property
        Note this makes complex animations with multiple transition-durations impossible
        Sorry
      */
      let propToWatch = '';
          
      // On panel finishing open/close animation
      contents.addEventListener("transitionend", (e) => {
        // Ignore transitions from child elements
        if(e.target !== contents) {
          return;
        }
          
        // Set property to watch on first fire
        if ( !propToWatch ) propToWatch = e.propertyName;
        
        // If watched property matches currently animating property
        if ( e.propertyName == propToWatch ) {
          let wasOpened = panel.classList.contains(this.settings.activeClass);
          wasOpened ? this._endOpen(panel) : this._endClose(panel);
        }
      });
    });
  }

  init() {
    // Attach functionality
    this._attachEvents();
    
    // If accordion, open the first panel
    if (this.settings.accordion) {
      this.openSinglePanel(this._panels[0]);
    }
    
    // For styling purposes
    this._container.classList.add(this.settings.initClass);
    
    return this;
  }
}

let makeMePretty = document.querySelector(".collapse");
let accordion = new Collapse(makeMePretty, { accordion: true }).init();

// Toggle accordion behavior
document.querySelector("#accordion-toggle")
  .addEventListener("change", function() {
    this.checked ? 
      accordion.settings.accordion = false : 
      accordion.settings.accordion = true ;
});

// hoisthoistupwego I'm stuck on a machine with IE11
function miscPolyfillsForIE() {  
  // NodeList.forEach() polyfill
  // https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Browser_Compatibility
  if (window.NodeList && !NodeList.prototype.forEach) {
    NodeList.prototype.forEach = Array.prototype.forEach;
  }

  // Object.assign() polyfill 
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
  "function"!=typeof Object.assign&&Object.defineProperty(Object,"assign",{value:function(e,t){"use strict";if(null==e)throw new TypeError("Cannot convert undefined or null to object");for(var n=Object(e),r=1;r<arguments.length;r++){var o=arguments[r];if(null!=o)for(var c in o)Object.prototype.hasOwnProperty.call(o,c)&&(n[c]=o[c])}return n},writable:!0,configurable:!0});
  
  // CustomEvent polyfill
  // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
  !function(){if("function"==typeof window.CustomEvent)return!1;function t(t,e){e=e||{bubbles:!1,cancelable:!1,detail:void 0};var n=document.createEvent("CustomEvent");return n.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),n}t.prototype=window.Event.prototype,window.CustomEvent=t}();
}
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.jsdelivr.net/gh/javan/details-element-polyfill@master/dist/details-element-polyfill.js