<div class="space-y-8">
  <h1 class="text-3xl font-bold mb-5">Accessible smooth collapse experiment</h1>
  
  <p>Primis ullamcorper laoreet morbi ex sem id, sapien molestie torquent aptent vivamus, felis dapibus diam lectus donec. <a href="https://wolstenhol.me" target="_top">Elit aenean eu eleifend vestibulum</a> sem quis commodo pretium sed rutrum aptent, proin cursus nunc sollicitudin vehicula dapibus iaculis est malesuada lobortis ante eget, nostra molestie morbi natoque luctus tortor bibendum taciti amet tristique.</p>

  <div 
    x-data="collapse"
    class="collapse border py-3 px-5 space-y-3"
    @resize.window.debounce="updateHeight"
  >
    <h2>
      <button 
        class="flex items-center w-full space-x-3 font-bold text-xl select-none text-left" 
        @click="toggle" 
        id="collapse-1-button" 
        aria-controls="collapse-1-content" 
        :aria-expanded="expanded ? 'true' : 'false'"
      >
        <svg
          class="flex-shrink-0 w-6 h-6"
          aria-hidden="true"
          focusable="false"
          xmlns="http://www.w3.org/2000/svg"
          class="h-6 w-6"
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
        >
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> </svg>
        <span>Expandable region heading</span>
      </button>
    </h2>
    <section 
      class="space-y-2 overflow-hidden transition-all duration-300 ease-out" 
      x-ref="content" 
      aria-labelledby="collapse-1-button"
    >
      <p>Congue interdum nibh at lobortis habitasse vehicula facilisis auctor, convallis nascetur aenean mattis egestas neque ante molestie penatibus, cras efficitur cursus class magnis quam tempus.</p>
      <p>
        <a href="https://wolstenhol.me" target="_top">I am a link that should only be focusable when the element is open</a>.
      </p>
      <p>Vehicula cursus velit elit placerat porta urna sit risus etiam, ullamcorper turpis praesent non tempus volutpat mus. Blandit magna hac auctor nec aliquet consequat orci maximus, rhoncus litora ultricies taciti tempor class. A mus consectetur nisl tellus cras blandit quisque consequat, duis aliquet rutrum aptent fermentum metus ligula, sapien penatibus venenatis potenti nisi eu eros. Turpis quam vestibulum praesent imperdiet amet curae hendrerit semper, ullamcorper metus lacinia cras lacus senectus sed. Dictumst torquent integer interdum mus consectetur efficitur natoque ultricies nec sodales auctor suspendisse orci cubilia, parturient egestas ligula est metus non scelerisque netus lacus imperdiet blandit pellentesque inceptos. Duis ad justo gravida turpis quam dui consequat dictumst, metus curabitur nisl magna vestibulum ex tempus sollicitudin morbi, interdum montes lacus vel sit facilisis sociosqu.</p>
    </section>
  </div>


  <p>Platea ligula justo auctor elit cubilia velit laoreet viverra, mollis feugiat amet dignissim phasellus aenean sed dui, tellus convallis at erat tortor donec efficitur. Litora aliquam habitasse ultrices dictumst torquent <a href="https://wolstenhol.me" target="_top">magna malesuada et urna magnis</a>, odio tristique praesent eu facilisi vel sed lacinia non, pulvinar tellus aliquet in ullamcorper consectetur lobortis metus imperdiet.</p>
</div>
.collapse__content {
  // We set the max-height using the --collapse-height
  // CSS custom property. If the custom property is 
  // missing then the fallback value of 0 is used.
  // By toggling the value of the custom property with
  // JavaScript we can animate the max-height.
  max-height: var(--collapse-height, 0);
}

// Unrelated to component - just for presentation of example content.
a {
  color: rgb(109, 40, 217);
  text-decoration: underline;

  &:hover,
  &:focus {
    color: rgb(91, 33, 182);
  }
}
View Compiled
document.addEventListener('alpine:init', () => {
  Alpine.data('collapse', () => ({
    expanded: null,
    init() {
      const elem = this.$refs.content;

      // We add the hidden attribute and CSS class via Alpine's init function
      // so that content is not hidden if the JS fails to execute. The downside
      // of this is a layout-shift issue and a flash of content appearing then
      // disappearing as the page loads then the JS executes. The balance
      // between progressive enhancement and layout-shift issues is tricky!
      elem.hidden = true;
      elem.classList.add('collapse__content');
    },
    toggle() {
      const elem = this.$refs.content;

      if (this.expanded) {
        // Remove the --collapse-height custom property so the browser uses
        // the fallback value of 0.
        elem.style.removeProperty('--collapse-height');
        elem.addEventListener(
          'transitionend',
          (e) => {
            // We need to make sure the event hasn't come from a child element
            // and bubbled up to our element.
            if (e.target === elem) {
              // Mark the element as hidden so its contents will be
              // hidden from assistive tech like screen readers or
              // keyboard navigation.
              elem.hidden = true;
              this.expanded = false;
            }
          },
          {
            once: true,
          }
        );
      } else {
        // Unhide our element so we can calculate its dimensions.
        // It will still be visually hidden because of the maxHeight
        // of 0.
        elem.hidden = false;
        // Set a --collapse-height property that matches the elements height.
        // This will cause the browser to animate the opening of the
        // element.
        elem.style.setProperty('--collapse-height', `${elem.scrollHeight}px`);
        elem.addEventListener(
          'transitionend',
          (e) => {
            // We need to make sure the event hasn't come from a child element
            // and bubbled up to our element.
            if (e.target === elem) {
              this.expanded = true;
            }
          },
          {
            once: true,
          }
        );
      }
    },
    updateHeight() {
      // A function to update the custom property on window resize, to avoid
      // clipping content if the scrollHeight value of the element changes.
      if (this.expanded) {
        const elem = this.$refs['content'];
        elem.style.setProperty('--collapse-height', `${elem.scrollHeight}px`);
      }
    },
  }));
});

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.7/tailwind.min.css

External JavaScript

This Pen doesn't use any external JavaScript resources.