Info en relaties en semantische HTML

Als je een website gebruikt zonder dat je het scherm kunt zien. Hoe weet je dan:

  • Welke tekst een kop is en welk niveau deze heeft?
  • Welke items bij elkaar horen in een lijst?
  • Welke cellen bij welke rijen of kolommen horen in een tabel?

In dit artikel leg ik uit hoe je met semantische HTML de structuur van je pagina vastlegt in de code. Dat maakt je content begrijpelijk voor iedereen, ook voor mensen die hulptechnologie gebruiken.

Dit valt onder succescriterium 1.3.1 Info en relaties van de WCAG 2.2-richtlijnen.

Wat is 1.3.1 info en relaties?

Succescriterium 1.3.1 Info en relaties stelt:

Informatie, structuur en relaties overgebracht door presentatie kunnen door software bepaald worden of zijn beschikbaar in tekst.

Simpel gezegd: alles wat je visueel overbrengt met opmaak, kleur of positie, moet ook in de code zijn vastgelegd. Een schermlezer leest de code, niet het scherm. Gebruik je alleen CSS om tekst er als een kop uit te laten zien? Dan ziet een schermlezer geen kop, maar gewone tekst.

Semantische HTML is de belangrijkste manier om aan dit criterium te voldoen. Maar het gaat breder dan HTML-structuur alleen. Ook informatie die door andere visuele middelen wordt overgebracht, moet programmatisch bepaalbaar zijn of beschikbaar zijn in tekst.

Vergelijk deze twee voorbeelden:

<!-- Niet-semantisch -->
<div class="title">Mijn hoofdkop</div>
<div class="subtitle">Mijn subkop</div>

<!-- Semantisch -->
<h1>Mijn hoofdkop</h1>
<h2>Mijn subkop</h2>

Voor iemand die goed kan zien is er misschien geen verschil als je de juiste CSS toepast, maar voor een schermlezer is er wél verschil. In het semantische voorbeeld weet hulptechnologie dat het met koppen te maken heeft en wat hun onderlinge hiërarchie is.

Koppen

Koppen zorgen voor structuur op je pagina. Ze helpen mensen die een schermlezer gebruiken om snel te navigeren. Door op één toets te drukken springen zij van kop naar kop. Als de kopstructuur niet klopt, is dat alsof je de inhoudsopgave van een boek door elkaar gooit.

In de Rotor van VoiceOver ziet dit er zo uit:

Schermafbeelding van een VoiceOver Rotor met een overzicht van koppen van niveau 1, 2, 3, en 4.

Hoe werkt het?

Gebruik de HTML-elementen <h1> tot en met <h6>. De nummering geeft de hiërarchie aan. Begin de hoofdinhoud van elke pagina met een <h1> en werk vervolgens door naar <h2>, <h3>, enz.

<!-- Goed voorbeeld -->
<h1>Productoverzicht</h1>
  <h2>Elektronica</h2>
    <h3>Smartphones</h3>
    <h3>Laptops</h3>
  <h2>Kleding</h2>
    <h3>Dames</h3>
    <h3>Heren</h3>

<!-- Slecht voorbeeld -->
<div class="heading">Productoverzicht</div>
<div class="subheading">Elektronica</div>

Als ARIA-alternatief kun je role="heading" met aria-level gebruiken. Dit is een volwaardig alternatief als een <hx>-element niet beschikbaar is:

<div role="heading" aria-level="2">Elektronica</div>

Best practices voor koppen

Vermijd zo veel mogelijk het overslaan van kopniveaus. Ga van h1 naar h2 naar h3 en spring dus niet van h1 naar h3. Begin de hoofdinhoud van elke pagina met een <h1>, beschrijf daarin het onderwerp van de pagina. Zo kunnen mensen die een schermlezer gebruiken meteen zien wat de pagina over gaat.

<!-- Slecht voorbeeld -->
<h1>Productoverzicht</h1>
<h3>Elektronica</h3> <!-- Sprong van h1 naar h3 -->
<h6>Smartphones</h6> <!-- Sprong van h3 naar h6 -->

<!-- Goed voorbeeld -->
<h1>Productoverzicht</h1>
  <h2>Elektronica</h2>
    <h3>Smartphones</h3>
    <h3>Laptops</h3>
  <h2>Kleding</h2>
    <h3>Dames</h3>
    <h3>Heren</h3>

Veelgemaakte fouten

  • Handmatige opmaak van tekst: Vetgedrukte of grotere tekst zonder kop-element ziet er visueel uit als een kop, maar is dat niet in de code. Een schermlezer kondigt het niet aan als kop.
  • Koppen gebruiken voor opmaak: Kop-element gebruiken omdat je grotere tekst wilt, terwijl de tekst geen kop is. Een schermlezer kondigt dan een kop aan die geen kop is.

Lijsten

Lijsten maken je tekst makkelijker te lezen en te begrijpen. Schermlezers kondigen het type lijst aan en hoeveel items er in de lijst zitten. Zo weet de gebruiker wat te verwachten.

Soorten lijsten

Er zijn drie soorten lijsten in HTML, elk met een eigen gebruik:

  • Ongeordende lijst of opsomming <ul>: Gebruik deze als de volgorde niet uitmaakt.
  • Geordende of genummerde lijst <ol>: Gebruik deze als de volgorde juist wél uitmaakt, zoals bij een stappenplan.
  • Definitielijst <ul>: Gebruik voor termen met beschrijvingen, zoals een woordenlijst of FAQ.
<!-- Ongeordende lijst -->
<ul>
  <li>Appels</li>
  <li>Bananen</li>
  <li>Peren</li>
</ul>

<!-- Geordende lijst -->
<ol>
  <li>Meng de ingrediënten</li>
  <li>Bak het deeg</li>
  <li>Laat het afkoelen</li>
</ol>

<!-- Definitielijst -->
<dl>
  <dt>HTML</dt>
  <dd>HyperText Markup Language, de standaardtaal voor webpagina's</dd>
  <dt>CSS</dt>
  <dd>Cascading Style Sheets, voor de opmaak van webpagina's</dd>
</dl>

Kies bewust een passend lijsttype. Een ongeordende lijst voor stappen in een recept klopt niet. Een geordende lijst voor een rij losse kenmerken ook niet. Als ARIA-alternatief bij maatwerk componenten kun je role="list" en role="listitem" gebruiken.

Veelgemaakte fouten

  • Handmatige nummering of opsommingstekens: Tekst zoals “- Eerste item” of “1. Tweede item” ziet er visueel uit als een lijst, maar is dat niet in de code.
  • Ongeldige kind-elementen: De HTML-specificatie staat alleen <li>, <script> en <template> toe als directe kinderen van <ul> en <ol>. Alles anders is ongeldig. Browsers repareren de DOM vaak automatisch, maar dat levert verborgen structuurproblemen op die schermlezergedrag beïnvloeden.
  • Onjuist geneste lijsten: Geneste lijsten horen altijd binnen een <li>-element te staan, niet direct in <ul>.
<!-- Slecht voorbeeld -->
<div>- Eerste item</div>
<div>- Tweede item</div>
<div>- Derde item</div>

<!-- Goed voorbeeld -->
<ul>
  <li>Eerste item</li>
  <li>Tweede item</li>
  <li>Derde item</li>
</ul>

<!-- Slecht voorbeeld -->
<ul>
  <div>
    <li>Item 1</li>
    <li>Item 2</li>
  </div>
</ul>

<!-- Goed: <div> binnen <li> -->
<ul>
  <li>
    <div>Item 1</div>
  </li>
  <li>
    <div>Item 2</div>
  </li>
</ul>

Tabellen

Tabellen gebruik je om gegevens te presenteren of te vergelijken. Ze zijn bedoeld voor tabulaire gegevens, niet voor layout. Een datatabel heeft altijd twee dimensies: rijen en kolommen met een relatie tussen de cellen en de koppen.

Een toegankelijke tabel maken

Een toegankelijke tabel heeft altijd:

  • <th>: Tabelkoppen (met scope-attribuut)
  • <td>: Gegevenscellen
  • <caption>: Beschrijft waar de tabel over gaat
  • <thead><tbody><tfoot>: Structureren de tabel in logische secties
<table>
  <caption>Aantal succescriteria per WCAG niveau</caption>
  <thead>
    <tr>
      <th scope="col">WCAG versie</th>
      <th scope="col">Niveau A</th>
      <th scope="col">Niveau AA</th>
      <th scope="col">Niveau AAA</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">WCAG 2.1</th>
      <td>30</td>
      <td>50</td>
      <td>78</td>
    </tr>
    <tr>
      <th scope="row">WCAG 2.2</th>
      <td>30</td>
      <td>54</td>
      <td>86</td>
    </tr>
  </tbody>
</table>

Het scope-attribuut is belangrijk. Het vertelt hulptechnologie of een tabelkop bij een rij of kolom hoort. Gebruik dit attribuut altijd als er koppen in twee richtingen zijn. Gebruik de waarde col voor kolom en row voor rij. Bij gegroepeerde koppen gebruik je daarnaast scope="colgroup" en scope="rowgroup".

Complexe tabellen

Voor complexere tabellen met tabelkoppen op meerdere niveaus gebruik je headers en id:

<table>
  <caption>Kwartaalcijfers per afdeling</caption>
  <tr>
    <td></td>
    <th id="q1">Kwartaal 1</th>
    <th id="q2">Kwartaal 2</th>
    <th id="q3">Kwartaal 3</th>
    <th id="q4">Kwartaal 4</th>
  </tr>
  <tr>
    <th id="sales">Verkoop</th>
    <td headers="sales q1">€10K</td>
    <td headers="sales q2">€12K</td>
    <td headers="sales q3">€15K</td>
    <td headers="sales q4">€18K</td>
  </tr>
  <tr>
    <th id="marketing">Marketing</th>
    <td headers="marketing q1">€5K</td>
    <td headers="marketing q2">€6K</td>
    <td headers="marketing q3">€7K</td>
    <td headers="marketing q4">€8K</td>
  </tr>
</table>

Door de headers-attributen kan een schermlezer hier aankondigen: “Verkoop Kwartaal 1 €10K”. Zorg dat id-waarden uniek zijn op de pagina. Typfouten in id– of headers-waarden zijn een veelgemaakte fout waardoor er geen koppeling is.

Veelgemaakte fouten

  • Datatabellen zonder tabelkoppen: Gegevenscellen zonder zijn niet te onderscheiden van gewone cellen. Een schermlezer leest alleen de waarden voor, zonder context.
  • Tabellen voor lay-out: Gebruik CSS Flexbox of Grid voor kolommen naast elkaar, niet een tabel.
<!-- Slecht voorbeeld -->
<table>
  <tbody>
    <tr>
      <th scope="row">Januari</th>
      <td>€750</td>
      <td>€300</td>
      <td>€1050</td>
    </tr>
    <tr>
      <th scope="row">Februari</th>
      <td>€750</td>
      <td>€280</td>
      <td>€1030</td>
    </tr>
  </tbody>
</table>

<!-- Goed voorbeeld -->
<table>
  <caption>Maandelijkse uitgaven</caption>
  <thead>
    <tr>
      <th scope="col">Maand</th>
      <th scope="col">Huur</th>
      <th scope="col">Eten</th>
      <th scope="col">Totaal</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Januari</th>
      <td>€750</td>
      <td>€300</td>
      <td>€1050</td>
    </tr>
    <tr>
      <th scope="row">Februari</th>
      <td>€750</td>
      <td>€280</td>
      <td>€1030</td>
    </tr>
  </tbody>
</table>

Formulieren

Elk formulierelement moet een programmatisch gekoppeld label hebben. Zonder die koppeling weet een schermlezergebruiker niet welk veld focus heeft of welke informatie wordt verwacht.

Labels

Elk invoerveld moet een programmatisch gekoppeld label hebben. Dit doe je met het for-attribuut dat verwijst naar het id van het invoerveld, of door het invoerveld binnen het <label>-element te plaatsen.

<!-- for en id -->
<label for="email">E-mailadres</label>
<input type="email" id="email" name="email">

<!-- Nesting -->
<label>
  E-mailadres
  <input type="email" name="email">
</label>

Een schermlezer leest bij focus op het invoerveld automatisch het gekoppelde label voor. Zonder die koppeling weet de gebruiker niet welke informatie wordt verwacht.

Groepen van invoervelden

Groepeer gerelateerde invoervelden met <fieldset> en <legend>. Dit is vooral belangrijk bij keuzerondjes en selectievakjes die samen één vraag beantwoorden.

<fieldset>
  <legend>
    Bezorgvoorkeuren
  </legend>
  <label>
    <input type="radio" name="bezorging" value="thuis">
    Thuisbezorgen
  </label>
  <label>
    <input type="radio" name="bezorging" value="ophalen">
    Ophalen in winkel
  </label>
</fieldset>

De schermlezer kondigt eerst de <legend> aan (“Bezorgvoorkeuren”) en vervolgens elk individueel label. Zo begrijpt de gebruiker de context.

Bij lange keuzelijsten met logische groepen gebruik je om de opties te groeperen:

<label for="land">Land</label>
<select id="land">
  <optgroup label="Europa">
    <option value="nl">Nederland</option>
    <option value="be">België</option>
  </optgroup>
  <optgroup label="Noord-Amerika">
    <option value="us">Verenigde Staten</option>
    <option value="ca">Canada</option>
  </optgroup>
</select>

Verplichte velden

Als je verplichte velden aanduidt met een sterretje (*), leg dan in een instructie boven het formulier uit wat dat sterretje betekent. Voeg ook het HTML required-attribuut toe aan het invoerveld. Zo is de informatie zowel beschikbaar als tekst als programmatisch bepaalbaar.

Veelvoorkomende fouten

  • Label is visueel aanwezig maar niet gekoppeld: Het label staat naast het veld, maar er is geen for/id-koppeling. Klikken op de labeltekst doet dan niets en een schermlezer leest de koppeling niet voor.
  • Groepen selectievakjes die niet zijn gegroepeerd: Elk selectievakje heeft wel een label, maar de context van de groep ontbreekt.
<!-- Slecht voorbeeld -->
<span>
  E-mailadres
</span>
<input type="email" name="email">

<!-- Goed voorbeeld -->
<label for="email">
  E-mailadres
</label>
<input type="email" id="email" name="email">

<!-- Slecht voorbeeld -->
<p>
  Hoe wil je betalen?
</p>
<input type="radio" name="betaling" value="ideal">
iDEAL
<input type="radio" name="betaling" value="creditcard">
Creditcard

<!-- Goed voorbeeld -->
<fieldset>
  <legend>
    Hoe wil je betalen?
  </legend>
  <label>
    <input type="radio" name="betaling" value="ideal">
    iDEAL
  </label>
  <label>
    <input type="radio" name="betaling" value="creditcard">
    Creditcard
  </label>
</fieldset>

Oriëntatiepunten (landmarks)

De indeling van je pagina is ook onderdeel van 1.3.1 info en relaties. Oriëntatiepunten zoals de navigatie, de hoofdinhoud en een sidebar hebben elk een eigen betekenis. Die betekenis moet je ook vastleggen in de code.

Gebruik de HTML-elementen die de rol van elk oriëntatiepunt beschrijven:

  • <header>: de paginakop, met logo en navigatie
  • <nav>: een navigatieblok
  • <main>: de hoofdinhoud van de pagina
  • <aside>: aanvullende informatie, niet essentieel voor de hoofdinhoud
  • <footer>: de paginavoet

Een schermlezer kan zo onderscheid maken tussen de navigatie en de hoofdinhoud. Zonder deze elementen is elk blok voor hulptechnologie gewoon een <div>.

<header>
  <a href="/">Logo</a>
  <nav aria-label="Hoofdnavigatie">
    <ul>
      <li>
        <a href="/over">Over ons</a>
      </li>
    </ul>
  </nav>
</header>
<main>
  <h1>
    Productoverzicht
  </h1>
  ...
</main>
<footer>
  <p>
    Copyright
  </p>
</footer>

Gebruik je meerdere <nav>-elementen op een pagina, geef ze dan elk een toegankelijke naam om ze van elkaar te onderscheiden.

Let op: het <section>-element is alleen een oriëntatiepunt als het een toegankelijke naam heeft via aria-label of aria-labelledby. Zonder naam negeert hulptechnologie het als oriëntatiepunt. Gebruik dus bij voorkeur alleen met een naam.

Veelgemaakte fouten

  • Navigatieblokken worden niet opgemaakt met een <nav>-element: Een schermlezer kan geen onderscheid maken tussen navigatie en andere content.
  • Er zijn meerdere <nav>-elementen zonder onderscheidende naam: De schermlezer kondigt beide aan als “navigatie”, zonder te zeggen welke welke is.

Overige semantische HTML

Ook de betekenis van tekst moet programmatisch bepaalbaar zijn als die betekenis visueel wordt overgebracht.

Nadruk en bijzondere tekst

Gebruik semantische elementen als de opmaak betekenis overbrengt. <em> gebruik je voor klemtoon of nadruk. <strong> gebruik je voor sterk belang, zoals een waarschuwing.

<!-- Nadruk -->
<p>Je moet het formulier <em>vóór</em> 1 januari insturen.</p>

<!-- Sterk belang -->
<p><strong>Let op:</strong> dit kan niet ongedaan worden gemaakt.</p>

Citaten

Gebruik <blockquote> voor blokcitaten en <q> voor korte citaten in de lopende tekst. Let op: <q> heeft beperkte schermlezerondersteuning. Voeg aanhalingstekens in de tekst toe als aanvulling.

Bronvermelding hoort buiten het <blockquote>-element.

<figure>
  <blockquote>
    <p>Het internet is voor iedereen.</p>
  </blockquote>
  <figcaption>— Niek Derksen</figcaption>
</figure>

Code

Gebruik <code> voor inline code en <pre> voor codeblokken:

<!-- Inline code -->
<p>Gebruik het <code>alt</code>-attribuut voor tekstalternatieven.</p>

<!-- Codeblok -->
<pre><code>
function hallo() {
  console.log("Hallo wereld");
}
</code></pre>

Afbeeldingen met bijschrift

Gebruik <figure> en <figcaption> om een afbeelding programmatisch te koppelen aan het bijschrift:

<figure>
  <img src="diagram.png" alt="Diagram van de WCAG-richtlijnenstructuur">
  <figcaption>De vier principes van WCAG: waarneembaar, bedienbaar, begrijpelijk en robuust.</figcaption>
</figure>

Vereisten en aanbevelingen voor info en relaties

WCAG-vereisten

  • Koppen die visueel aanwezig zijn, zijn in de code gemarkeerd als kop met <h1>tot en met <h6> of role="heading" met aria-level.
  • Lijsten zijn opgemaakt met <ul>, <ol>, <dl> of role="list" met role="listitem".
  • Datatabellen hebben tabelkoppen met <th> en scope-attribuut. Complexe tabellen gebruiken headers en id.
  • Presentatietabellen hebben role="presentation" en geen tabelkoppen of <caption>.
  • Formulierelementen hebben een programmatisch gekoppeld label.
  • Groepen van keuzerondjes of selectievakjes zijn gegroepeerd met <fieldset> en <legend>.
  • Paginagebieden zijn gemarkeerd met <header>, <nav>, <main>, <aside> en <footer>.
  • Nadruk en bijzondere tekst die betekenis overbrengt, is gemarkeerd met <em> of <strong> of gelijkwaardige semantische elementen.
  • Citaten zijn gemarkeerd met <blockquote> of <q>.

Aanbevolen

  • Gebruik kopniveaus in volgorde en sla niveaus niet over.
  • Geef tabellen een bijschrift met <caption> of <figcaption>.
  • Groepeer opties in een <select> met <optgroup> als er logische groepen zijn.
  • Geef meerdere <nav>-elementen een onderscheidende aria-label.

Om te onthouden

Informatie die je visueel overbrengt, moet je ook vastleggen in de code. Doe je dat niet, dan missen mensen die hulptechnologie gebruiken de structuur van je pagina. Semantische HTML is daarvoor de belangrijkste aanpak: gebruik de elementen die de betekenis van je content beschrijven, niet alleen de weergave.

Heb je vragen over specifieke implementaties? Laat het me weten!