Fortsæt til indhold

Accordions

En accordion gør det muligt at skjule uddybende indhold bag en overskrift, mens brugeren efterfølgende selv kan vælge at få vist indholdet.
Accordions er meget anvendte på sider med ofte stillede spørgsmål (Frequently Asked Questions eller forkortet FAQ, som de også hedder på engelsk). I ny og næ kan vi også være heldige at få øje på dem på produktsider, hvor de også kan bruges til produktdetaljer.

Det er på mange måder et praktisk element, der kan bruges i flere sammenhænge, og med lidt ekstra kode og omtanke kan vi oven i købet gøre vores accordions tilgængelige for endnu flere mennesker.

For en gang skyld en utrolig kort indledning og forklaring af, hvad en accordion kan bruges til. Mit bedste bud er dog også at du allerede kender til accordions, så ingen grund til at trække tingene i langdrag.

Dansk oversættelse

Jeg ville virkelig ønske vi på dansk oversatte elementets navn direkte og brugte “harmonika”, selvom det desværre nok kun er mig, der synes det lyder fedt.
Ja, jeg er faktisk ikke sikker på elementet overhovedet har en dansk betegnelse — der er Norge [1] i øvrigt absolut et foregangsland — så indtil videre må jeg bruge den engelske betegnelse accordion.

Jeg har ikke direkte en fobi mod alle de engelske udtryk vi ofte bruger i min branche, men tit er der ikke noget i vejen for at beskrive elementet eller funktionaliteten på dansk. Den helt store fordel er nemlig at personer, der ikke kender til alle vores engelske eller tekniske begreber, også forstår hvad der snakkes om.

Som jeg startede med, så findes der “desværre” ikke et godt dansk ord for en harmonika… undskyld accordion. Det bliver måske også hurtigt en spøjs samtale, hvis man snakker om at harmonikaen ikke er tilgængelig, men omvendt kunne udtrykket “Din harmonika spiller bare!” måske få lidt medvind.Som jeg startede med, så findes der “desværre” ikke et godt dansk ord for en harmonika… undskyld accordion. Det bliver måske også hurtigt en spøjs samtale, hvis man snakker om at harmonikaen ikke er tilgængelig, men omvendt kunne udtrykket “Din harmonika spiller bare!” måske få lidt medvind.

Jeg vil ikke forsøge at opfinde et dansk ord, så jeg fortsætter med accordion (i hvert fald lidt endnu).

HTML

Ikke så mange dikkedarer [2], lad os springe ud i det, og bid for bid få sammensat vores HTML kode.

<div class="accordion js-hide">
  ...
</div>

Ikke så meget hokus pokus indtil videre. Klassen js-hide benyttes til at indikere om indholdet vises eller ej. Når klassen er sat på elementet skjules indholdet, og ellers vises indholdet.

Dernæst har vi brug for en overskrift, og et link (eller bare en almindelig knap) med nogle forskellige attributter, som er med til at sikre udseende, funktionalitet og ikke mindst tilgængelighed.

<div class="accordion js-hide">
  <h2><a href="#accordion-harmonika" id="accordion-button-harmonika" class="accordion-button" role="button" aria-expanded="false" aria-controls="accordion-harmonika" data-action="accordion-toggle">Få detaljer om harmonikaen</a></h2>
  ...
</div>

Her sker der lidt flere ting, så lad os lige gennemgå alle attributterne.

href
link til indholdet, og skal matche værdien i indholdets id.
id
benyttes her til at koble linket sammen med indholdet. Skal matche værdien i indholdets aria-labelledby.
class
er med så vi kan tilpasse linkets udseende, og få den tilpasset til websites design.
role
fortæller screen readers at linket i dette tilfælde skal opfattes som en knap.
aria-expanded
er vigtig for brugere med screen readers, da det fortæller dem om indholdet i vores accordion er skjult eller synligt. Er værdien sat til false bør indholdet være skjult, og omvendt med værdien true.
aria-controls
fortæller screen readers, hvilket indhold dette link styrer, og skal matche indholdets id.
data-action
er den måde jeg typisk kobler noget JavaScript funktionalitet sammen med et element.

Hvis nogle af værdierne i id og de forskellige aria-attributter ikke giver mening endnu, gør de det forhåbentlig lige om lidt, når vi har gennemgået selve indholdet, der som udgangspunkt skjules indtil brugeren har klikket på linket.

<div class="accordion js-hide">
  <h2><button id="accordion-button-harmonika" class="accordion-button" aria-expanded="false" aria-controls="accordion-harmonika" data-action="accordion-toggle">Få detaljer om harmonikaen</button></h2>
  <div id="accordion-harmonika" class="accordion-content" aria-hidden="true" aria-labelledby="accordion-button-harmonika">
    ...
  </div>
</div>

Her er igen lidt forskellige attributter, så vi tager dem en efter en.

id
benyttes her til at koble indholdet sammen med linket. Skal matche værdien i linkets aria-controls.
class
giver os, som med linket, mulighed for at tilpasse udseendet.
aria-hidden
fortæller skærmlæseren om indholdet er skjult og dermed også om det skal læses op, afhængigt af om det er skjult eller ej. Er værdien sat til false læses indholdet op, mens det er skjult for skærmlæseren når værdien er true, og læses derfor ikke op.
aria-labelledby
fortæller screen readers, hvilket element, der fungerer som en overskrift når indholdet læses op, og skal derfor matche linkets id.

Så nu er det blot op til dig at fylde noget indhold i.

CSS

Med vores HTML på plads, er det tid til at kigge på noget CSS. Jeg har holdt det helt simpelt, da du med al sandsynlighed alligevel har tænkt dig at tilpasse udseendet.

.accordion {
    border-bottom: 2px solid #243638;
    border-top: 2px solid #243638;
    margin: 2em 0;
}
.accordion h2 {
    margin-bottom: 0;
    margin-top: 0;
}
.accordion .accordion-button {
    background: none;
    border: none;
    border-radius: 0;
    color: currentColor;
    display: block;
    font: inherit;
    font-size: 1.4rem;
    line-height: 1em;
    padding: 1em 3.2em 1em 1em;
    position: relative;
    text-align: left;
    width: 100%;
}
.accordion .accordion-content {
    overflow: hidden;
}
.accordion.js-hide .accordion-content {
    height: 0;
    visibility: hidden;
}
@media (prefers-reduced-motion: no-preference) {
    .accordion .accordion-content {
   	 transition: height 0.6s, visibility 0s;
   	 will-change: height, visibility;
    }
    .accordion.js-hide .accordion-content {
   	 transition: height 0.6s, visibility 0s 0.6s;
    }
}
.accordion .accordion-content .inner {
    padding: 1em 2em;
}

Da jeg skrev at jeg ville holde det helt simpelt, løj jeg ikke…

Eneste lille krølle er den media query jeg har smidt ind. I mit eksempel animeres højden når indholdet skjules eller vises, men med den sørger jeg selvfølgelig for kun at animere højden, hvis brugeren ikke specifikt har indstillet sit styresystem eller browser til at minimere animerede objekter.

Er du helt ligeglad med animationen — browserindstilling eller ej — kan du selvfølgelig sagtens benytte display: none og display: block i stedet for at bøvle med højden.

JS

Det sidste kode er vores JavaScript, og samtidig den del med mest kode at beskrive, så sæt dig godt til rette, mens jeg prøver at bryde det op i så spiselige bider, som det giver mening.

const accordionChange = (element) => {
    if (element) {
        const accordion = element.closest('.accordion');
        const accordions = element.closest('.accordions');
        const accordionTarget = document.getElementById(element.getAttribute('aria-controls'));

        // Button
        element.setAttribute('aria-expanded', !(element.getAttribute('aria-expanded') === 'true'));
        element.focus();

        // Content
        accordionTarget.setAttribute('aria-hidden', !(accordionTarget.getAttribute('aria-hidden') === 'true'));
        accordionTarget.style.height = accordionTarget.scrollHeight + 'px';
        accordionTarget.style.overflow = 'hidden';

        if (!accordion.classList.contains('js-hide')) {
            setTimeout(() => {
                accordionTarget.style.height = '0';
            }, 20);
        }

        accordion.classList.toggle('js-hide');

        if (accordions) {
            accordions.querySelectorAll('.accordion').forEach((item) => {
                if (item !== accordion) {
                  // Button
                  accordion.querySelector('.accordion-button').setAttribute('aria-expanded', false);

                  // Content
                  if (!accordion.classList.contains('js-hide')) {
                      accordion.querySelector('.accordion-content').setAttribute('aria-hidden', true);
                      accordion.querySelector('.accordion-content').style.height = accordion.querySelector('.accordion-content').scrollHeight + 'px';
                      accordion.querySelector('.accordion-content').style.overflow = 'hidden';

                      setTimeout(() => {
                          accordion.querySelector('.accordion-content').style.height = '0';
                      }, 20);
                  }

                  accordion.classList.add('js-hide');
                }
            });
        }
    }
};

Lad os starte med det vigtigste, nemlig den funktion, der gør alt arbejdet med at ændre alle de nødvendige attributter, højde osv., når indholdet i vores accordion vises eller skjules.

Først finder vi frem til den aktuelle accordion, opdaterer de nødvendige aria-attributter og med if-sætningen kontrollerer vi derefter om den står alene eller om den er en del af en gruppe.
Står den alene, sættes en højde på indholdet (så den kan animeres) og vi tilføjer/fjerner klassen js-hide.
Har vi en gruppe med flere accordions, lukker vi samtidig de øvrige accordions i gruppen.
sætter en højde for indholdet så den kan animeres og tilføjer/fjerner klassen.

Med en funktion der holder styr på om attributter og klasser skal ændres, mangler vi blot at den funktion kaldes ved klik på et link eller en knap, og det er lige præcis hvad vores næste kodeblok gør.

const accordionToggle = (event) => {
    if (!(event.ctrlKey || event.metaKey || event.shiftKey)) {
   	 event.preventDefault();
    }
    const element = event.target;
    
    accordionChange(element);
};
const accordionButtonToggle = document.querySelectorAll('[data-action="accordion-toggle"]');
accordionButtonToggle.forEach((button) => {
    button.addEventListener('click', accordionToggle);
});

Vi tager også højde for klik, hvor CTRL (Windows), OPTION (OSX) eller SHIFT (OSX) tasterne holdes nede ved klik.
Holdes en af de taster nede ved klik, skal brugeren nemlig have lov til at åbne linket i en ny fane eller et nyt vindue.

window.addEventListener('keydown', (event) => {
    const focusedElement = document.activeElement;
    const accordions = document.activeElement.closest('.accordions');

    if (focusedElement && accordions) {
   	 // 32 (space) = toggle
   	 if (event.keyCode === 32) {
   		 event.preventDefault();
   		 accordionChange(focusedElement);
   	 }
   	 // 38 (up arrow) = prevoius
   	 if (event.keyCode === 38) {
   		 if (focusedElement.closest('.accordion').previousElementSibling && focusedElement.closest('.accordion').previousElementSibling.querySelector('.accordion-button')) {
   			 focusedElement.closest('.accordion').previousElementSibling.querySelector('.accordion-button').focus();
   		 }
   	 }
   	 // 40 (down arrow) = next
   	 if (event.keyCode === 40) {
   		 if (focusedElement.closest('.accordion').nextElementSibling && focusedElement.closest('.accordion').nextElementSibling.querySelector('.accordion-button')) {
   			 focusedElement.closest('.accordion').nextElementSibling.querySelector('.accordion-button').focus();
   		 }
   	 }
   	 // 36 (home) = first tab
   	 if (event.keyCode === 36) {
   		 if (accordions.querySelector('.accordion:first-child .accordion-button')) {
   			 event.preventDefault();
   			 accordions.querySelector('.accordion:first-child .accordion-button').focus();
   		 }
   	 }
   	 // 35 (end) = last tab
   	 if (event.keyCode === 35) {
   		 if (accordions.querySelector('.accordion:last-child .accordion-button')) {
   			 event.preventDefault();
   			 accordions.querySelector('.accordion:last-child .accordion-button').focus();
   		 }
   	 }
    }
});

Det sidste der er tilbage er brugen af keyboard. Noget af det, som gør at en accordion er tilgængelig, er at man kan benytte keyboardet til at skjule og vise indhold med.

Når knappen er i fokus, kan du benytte enten ENTER eller SPACE til at vise eller skjule indholdet afhængig af den aktuelle tilstand.
Er der indsat en gruppe af accordions er der desuden mulighed for at benytte PIL UP for at flytte fokus til forrige accordion, PIL NED for at flytte fokus til næste accordion, HOME for at flytte fokus til første accordion og END for at flytte fokus til sidste accordion - alt sammen indenfor gruppen.

Bonus funktionalitet

Til sidst kommer her lidt bonus funktionalitet, i mit lille bud på en tilgængelig accordion.

Jeg har nemlig nævnt noget om en gruppe et par gange. Det har jeg fordi der også er mulighed for at gruppere accordions, hvilket gør at når én åbnes lukkes de øvrige, så der kun er en åben ad gangen.

<div class="accordions">
  <div class="accordion js-hide">
    ...
  </div>
  <div class="accordion js-hide">
    ...
  </div>
</div>

Det gøres, som jeg har vist i eksemplet ovenfor, ved at tilføje <div class="accordions"> omkring de indsatte accordions.

Resultat

Som enhver god TV-kok har jeg selvfølgelig snydt lidt hjemmefra, og kodet noget på CodePen.

Eksempel på simpel accordion
Eksempel på accordion med animation
Eksempel på accordion med link i stedet for knap

Det er ikke sikkert at designet fra mit eksempel falder i din smag, eller måske kunne du godt bare tænke dig at se det på et rigtigt website. Derfor har jeg samlet tre links, der også benytter accordions på deres website - vær opmærksom på om de er tilgængelige eller ej!

https://www.smashingmagazine.com/2019/11/inclusive-components-prerelease/#table-of-contents\ https://mentordanmark.dk/ofte-stillede-sporgsmal\ https://www.datatilsynet.dk/hvad-siger-reglerne/ofte-stillede-spoergsmaal\ https://www.honeyflow.com/shop/flow-hive/flow-hive-2-cedar-6-frame/p/388#whats_included


Tak fordi du hang i, og gennemførte dette marathon indlæg.

Som jeg har nævnt et par gange i indlægget, så er dette min cover-version af en tilgængelig accordion — det er altså min egen måde at at gøre det på.

Det er selvfølgelig inspireret af andre tilgængelige accordions, og så har jeg også været ved at læse lidt dokumentation så jeg forstår hvilke dele af koden der gør min accordion tilgængelig.

Den viden har jeg forhåbentlig formået at videreformidle 🤞


  1. Norge insisterer nemlig på at oversætte fremmedord, så man læser lige ordene et par gange når man støder på “datorer”, “informasjonskapsler”, “smartklokker” og “nettbrett”, for at være sikker på man forstår hvad de betyder. ↩︎

  2. dikkedarer må endelig ikke forveksles med dromedarer, for dikkedarer har godt nok også kun én pukkel, men hverken en lang hals eller lange øjenvipper. ↩︎