Accordion (Multiselect)

Accordion (Multiselect)

Turn on a screen reader to experience this example in action.

Non-functional link, for testing purposes

HTML Source Code

<!---
This component has been adapted from an example provided by the W3C, in accordance with the W3C Software and Document License https://www.w3.org/copyright/software-license-2023/
-->
<div class="dqu-example">
<div id="accordionGroup" class="accordion">
    <h2>
        <button type="button"
              aria-expanded="true"
              class="accordion-trigger"
              aria-controls="sect1"
              id="accordion1id">
            <span class="accordion-title">
            Prospective Students
            <span class="accordion-icon"></span>
        </span>
      </button>
    </h2>
    <div id="sect1"
         role="region"
         aria-labelledby="accordion1id"
         class="accordion-panel">
         <ul>
            <li><a href="#">Prospective Students Home Page</a></li>
            <li><a href="#">Admissions</a></li>
            <li><a href="#">Financing Your Education</a></li>
        </ul>
    </div>
    <h2>
      <button type="button"
              aria-expanded="false"
              class="accordion-trigger"
              aria-controls="sect2"
              id="accordion2id">
            <span class="accordion-title">
            Students
            <span class="accordion-icon"></span>
        </span>
      </button>
    </h2>
    <div id="sect2"
         role="region"
         aria-labelledby="accordion2id"
         class="accordion-panel"
         hidden="">
         <ul>
            <li><a href="#">Students Home Page</a></li>
            <li><a href="#">Records</a></li>
            <li><a href="#">Student Life</a></li>
            <li><a href="#">Student Organizations</a></li>
            <li><a href="#">Technology</a></li>
            <li><a href="#">Library</a></li>
        </ul>
    </div>
    <h2>
      <button type="button"
              aria-expanded="false"
              class="accordion-trigger"
              aria-controls="sect3"
              id="accordion3id">
        <span class="accordion-title">
            Faculty
          <span class="accordion-icon"></span>
        </span>
      </button>
    </h2>
    <div id="sect3"
         role="region"
         aria-labelledby="accordion3id"
         class="accordion-panel"
         hidden="">
         <ul>
            <li><a href="#">Faculty Home</a></li>
            <li><a href="#">Directory</a></li>
            <li><a href="#">News</a></li>
            <li><a href="#">Publications</a></li>
        </ul>
         
    </div>
    <h2>
        <button type="button"
                aria-expanded="false"
                class="accordion-trigger"
                aria-controls="sect4"
                id="accordion4id">
          <span class="accordion-title">
            Alumni
            <span class="accordion-icon"></span>
          </span>
        </button>
      </h2>
      <div id="sect4"
           role="region"
           aria-labelledby="accordion4id"
           class="accordion-panel"
           hidden="">
           <ul>
            <li><a href="#">Alumni Home Page</a></li>
            <li><a href="#">Benefits and Services</a></li>
            <li><a href="#">Giving</a></li>
            <li><a href="#">Alumni Association</a></li>
            <li><a href="#">Volunteer Opportunities</a></li>
        </ul>
      </div>

</div>
</div>

JavaScript Source Code

Dependencies:
  • JQuery
//This component has been adapted from an example provided by the W3C, in accordance with the W3C Software and Document License https://www.w3.org/copyright/software-license-2023/

if( typeof TabAccordion === "undefined")
{    
    var TabAccordions = [];    
    class TabAccordion {
        static openedEles = new Map();
        constructor(domNode, multiple ) {
        if (typeof multiple == "undefined") multiple=true;
        if (typeof multiple == "boolean") this.multiple = multiple;
            
        this.rootEl = domNode;
        this.buttonEl = this.rootEl.querySelector('button[aria-expanded]');
        if ( this.buttonEl == null)
        {
            console.log("button and/or aria-expanded attribute is missing.");
            return null;
        }    

        const controlsId = this.buttonEl.getAttribute('aria-controls');
        this.contentEl = document.getElementById(controlsId);
        this.open = this.buttonEl.getAttribute('aria-expanded') === 'true';
        this.closeActiveTab();
            
        // add event listeners
        this.buttonEl.addEventListener('click', this.onButtonClick.bind(this));
        }
    
        onButtonClick() {
        this.toggle(!this.open);
        }
    
        toggle(open) {
        // don't do anything if the open state doesn't change
        if (open === this.open) {
            return;
        }
        // update the internal state
        this.open = open;
        // handle DOM updates
        this.buttonEl.setAttribute('aria-expanded', `${open}`);
        if (open) {
            this.contentEl.removeAttribute('hidden');
        } else {
            this.contentEl.setAttribute('hidden', '');
        }
        this.closeActiveTab();
        }
    
        closeActiveTab()
        {
            if (this.multiple ) return;
            var group = this.rootEl.closest('.accordion') || this.rootEl.parentElement;
            if(this.open)
            {
                var prev = TabAccordion.openedEles.get(group);
                if( prev != null )
                {
                    prev.contentEl.setAttribute('hidden', '');
                    prev.open = false;
                    prev.buttonEl.setAttribute('aria-expanded', 'false');
                }
                TabAccordion.openedEles.set(group, this);
            }
            else
            {
                if(this == TabAccordion.openedEles.get(group))
                    TabAccordion.openedEles.delete(group);
            }
        }

        closeOthers()
        {
            const accordions = document.querySelectorAll('.accordion h2');
            TabAccordions.forEach((accordionEl) => {
                let tab = accordionEl;
                if (tab.contentEl.id !== this.contentEl.id)
                    tab.close();    
            });
        }
        // Add public open and close methods for convenience
        open() {
        this.toggle(true);
        }
        close() {
        this.toggle(false);
        }

    }
  
    // init accordions
    const accordions = document.querySelectorAll('.accordion h2');
    accordions.forEach((accordionEl) => {
        var tab = new TabAccordion(accordionEl);
        TabAccordions.push(tab); 
    });

}

  

CSS Source Code

/*
  Accordion (Single-Select) — Restyled
  Deque University ARIA Component
  Design tokens from: Deque University Redesign (Bobby's Figma)

  This component has been adapted from an example provided by the W3C,
  in accordance with the W3C Software and Document License
  https://www.w3.org/copyright/software-license-2023/
*/

/* ========================================
   Design Tokens
   ======================================== */
.accordion {
  --dqu-bg-primary: #fcfaf8;
  --dqu-bg-secondary: #f6f3ed;
  --dqu-text-primary: #21201e;
  --dqu-border-primary: #beb7aa;
  --dqu-border-secondary: #8c827d;
  --dqu-interactive: #2e5f7a;
  --dqu-interactive-light: rgba(46, 95, 122, 0.08);
  --dqu-interactive-hover: #3a7a9a;
  --dqu-interactive-active: #254c62;
  --dqu-font-family: "Noto Sans", sans-serif;
  --dqu-radius: 8px;
}

/* ========================================
   Container
   ======================================== */
.accordion {
  margin: 0;
  padding: 0;
  border: 1px solid var(--dqu-border-secondary);
  border-radius: var(--dqu-radius);
  width: 24em;
  font-family: var(--dqu-font-family);
  background: var(--dqu-bg-primary);
  overflow: hidden;
}

.accordion:focus-within {
  border-color: var(--dqu-interactive);
  box-shadow: 0 0 0 1px var(--dqu-interactive);
}

.accordion h2 {
  margin: 0;
  padding: 0;
}

.accordion > * + * {
  border-top: 1px solid rgba(255, 255, 255, 0.2);
}

/* ========================================
   Trigger Button
   ======================================== */
.accordion-trigger {
  background: var(--dqu-interactive);
  color: #ffffff;
  display: flex;
  align-items: center;
  justify-content: space-between;
  position: relative;
  font-family: var(--dqu-font-family);
  font-weight: 500;
  font-size: 1rem;
  line-height: 1.5;
  width: 100%;
  margin: 0;
  padding: 12px 16px;
  border: none;
  border-radius: 0;
  cursor: pointer;
  text-align: left;
  transition: background-color 0.15s ease;
}

/* Hover and focus-visible: invert — light teal background + dark teal text
   + inset teal ring. Both outline and inset box-shadow used so the ring
   survives any global outline:none rules on the host page (WCAG 2.4.11). */
.accordion-trigger:hover,
.accordion-trigger:focus-visible {
  background: var(--dqu-interactive-light);
  color: var(--dqu-interactive);
  outline: 3px solid var(--dqu-interactive) !important;
  outline-offset: -3px;
  box-shadow: inset 0 0 0 3px var(--dqu-interactive);
  z-index: 1;
}

/* Remove default focus outline for mouse users */
.accordion-trigger:focus:not(:focus-visible) {
  outline: none !important;
}

.accordion:focus-within h2 {
  /* No special style needed — focus-visible on trigger handles it */
}

/* First / last triggers absorb the container's interior corner radius
   (8px container - 1px border = 7px) so focus/hover backgrounds curve
   to match the rounded card. Last-child scoped to collapsed state so
   the trigger stays square-bottom when joined to its expanded panel. */
.accordion > h2:first-of-type .accordion-trigger {
  border-radius: 7px 7px 0 0;
}
.accordion > h2:last-of-type .accordion-trigger[aria-expanded="false"] {
  border-radius: 0 0 7px 7px;
}

/* ========================================
   Trigger Title
   ======================================== */
.accordion-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  pointer-events: none;
  border: transparent 2px solid;
  border-radius: 4px;
  padding: 0;
  outline: none;
}

.accordion-trigger:focus .accordion-title {
  border-color: transparent; /* Using focus-visible on trigger instead */
}

/* ========================================
   Chevron Icon
   ======================================== */
.accordion-icon {
  border: solid #ffffff;
  border-width: 0 2px 2px 0;
  height: 0.5rem;
  width: 0.5rem;
  pointer-events: none;
  position: absolute;
  right: 1.25em;
  top: 50%;
  transform: translateY(-60%) rotate(45deg);
  transition: transform 0.2s ease;
  flex-shrink: 0;
}

.accordion-trigger:hover .accordion-icon,
.accordion-trigger:focus-visible .accordion-icon {
  border-color: var(--dqu-interactive);
}

.accordion-trigger[aria-expanded="true"] .accordion-icon {
  transform: translateY(-50%) rotate(-135deg);
}

/* ========================================
   Panel Content
   ======================================== */
.accordion-panel {
  margin: 0;
  background: var(--dqu-bg-primary);
}

.accordion-panel[hidden] {
  display: none;
}

.accordion-panel ul {
  margin: 0;
  padding: 0;
}

.accordion-panel li {
  list-style-type: none;
  background: var(--dqu-bg-primary);
  border-bottom: 1px solid var(--dqu-border-secondary);
  margin: 0;
  padding: 0;
}

.accordion-panel li:last-child {
  border-bottom: none;
}

.accordion-panel li a {
  display: block;
  padding: 10px 16px;
  color: var(--dqu-interactive);
  text-decoration: underline;
  font-size: 0.9375rem;
  line-height: 1.5;
  transition: background-color 0.15s ease;
}

/* Hover and focus-visible share one indicator — light teal background +
   inset teal outline — so keyboard and pointer users get the same
   high-contrast affordance (WCAG 2.4.11). */
.accordion-panel li a:hover,
.accordion-panel li a:focus-visible {
  outline: 3px solid var(--dqu-interactive) !important;
  outline-offset: -3px;
  background: var(--dqu-interactive-light);
  text-decoration: underline;
}

/* ========================================
   Legacy / Override Cleanup
   ======================================== */
.dqu-example button {
  border-style: none;
}

.accordion button::-moz-focus-inner {
  border: 0;
}

/* Legacy tab styles — kept for compatibility but restyled */
.accordion div.tab:focus {
  background-color: var(--dqu-interactive-light);
  outline: 2px solid var(--dqu-interactive) !important;
  outline-offset: -2px;
}

.accordion > li:hover {
  cursor: pointer;
}

.accordion div.tab {
  background: var(--dqu-bg-secondary);
  border: 1px solid var(--dqu-border-secondary);
  color: var(--dqu-text-primary);
  padding: 3px 6px;
}

.tablist div.tab.active {
  font-weight: bold;
}

.accordion-panel .inactive {
  display: none;
}

Copy and Paste Full Page Example