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.

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 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. (Informatie die je alleen met kleur overbrengt, valt onder 1.4.1 Gebruik van kleur.)

Semantische HTML is de belangrijkste manier om aan dit criterium te voldoen. Het draait om je bedoeling met de content, want met het element dat je kiest, vertel je wat iets is. Een <h1> zegt “dit is de hoofdkop” en een <ul> zegt “dit is een lijst met zoveel items”. Klopt je bedoeling niet met je code (bijvoorbeeld een <div> waar je eigenlijk een kop bedoelt), dan komt die betekenis niet aan bij hulptechnologie.

Kan je iets echt niet in de code vastleggen? Beschrijf het dan in tekst, dicht bij de content waar het over gaat.

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 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>

Kan je geen kop-element gebruiken, bijvoorbeeld in een maatwerk component? Gebruik dan role="heading" met aria-level. Een echt kop-element heeft altijd de voorkeur.

<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> die het onderwerp van de pagina beschrijft. Zo hoort iemand die een schermlezer gebruikt meteen waar 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: Je gebruikt een kop-element omdat je grotere tekst wilt, maar de tekst is geen kop. Een schermlezer kondigt dan een kop aan die geen kop is.

Lijsten

Lijsten maken je tekst makkelijker te lezen. Een schermlezer kondigt aan dat er een lijst begint en hoeveel items erin staan. Zo weet je meteen wat er komt.

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 <dl>: Gebruik deze voor termen met een beschrijving, zoals een woordenlijst of veelgestelde vragen
<!-- 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.
  • Andere elementen direct in de lijst: Direct in een <ul> of <ol> hoort alleen een <li>. Staat er iets anders tussen, zoals een <div>? Dan kondigt een schermlezer de lijst soms verkeerd aan.
  • 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 datatabel heeft altijd tabelkoppen die in de code aan de cellen zijn gekoppeld. Dat regel je met:

  • <th>: tabelkoppen, met het scope-attribuut om de richting aan te geven
  • <td>: gegevenscellen

De volgende onderdelen zijn niet verplicht maar maken je tabel duidelijker:

  • <caption>: beschrijft waar de tabel over gaat
  • <thead>, <tbody>, <tfoot>: delen de tabel op 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

  • Tabelkoppen die alleen visueel een kop zijn: De koppen zijn vet of hebben een achtergrondkleur, maar staan in een <td>. Een schermlezer noemt dan niet bij welke kop een gegevenscel hoort.
  • Tabelkoppen in een tabel voor lay-out: Gebruik je er een tabel alleen voor de lay-out, zet er dan geen <th>, <caption> of scope in. Anders kondigt een schermlezer de tabel aan als datatabel. Gebruik voor kolommen naast elkaar liever CSS Flexbox of Grid.
<!-- 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

Geef elk formulierelement een zichtbaar label en koppel dat label in de code aan het formulierelement. Zonder die koppeling weet iemand die een schermlezer gebruikt niet welke informatie een veld verwacht.

Labels

Koppel een label met het for-attribuut dat verwijst naar het id van het formulierelement. Of plaats het formulierelement binnen het <label>-element.

<!-- 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>

Krijgt het invoerveld de focus? Dan leest een schermlezer het gekoppelde label voor.

Groepen van formulierelementen

Groepeer formulierelementen die bij elkaar horen met <fieldset> en <legend>. Doe dat in elk geval 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>

Een schermlezer leest eerst de <legend> voor (“Bezorgvoorkeuren”) en daarna het label van elke keuze. Zo weet je bij welke vraag de keuzes horen.

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

Geef elk verplicht veld het required-attribuut, of aria-required="true" bij een maatwerk formulierelement. Zet novalidate op het <form>-element als het formulier eigen foutmeldingen heeft. Dan toont de browser geen eigen foutmeldingen.

Geef je verplichte velden aan met een sterretje (*)? Zet het sterretje dan binnen het <label>-element en leg boven het formulier uit wat het betekent.

<form novalidate>
  <p>
    Velden met een * zijn verplicht.
  </p>
  <label for="naam">
    Naam *
  </label>
  <input type="text" id="naam" required>
</form>

Foutmeldingen

Koppel elke foutmelding in de code aan het formulierelement waar die bij hoort. Dat doe je met aria-describedby. Een schermlezer leest de foutmelding dan voor zodra het veld de focus krijgt.

<label for="postcode">
  Postcode
</label>
<input type="text" id="postcode" aria-describedby="postcode-fout">
<p id="postcode-fout">
  Vul een postcode in, bijvoorbeeld 1234 AB.
</p>

Veelgemaakte fouten

  • Label is zichtbaar maar niet gekoppeld: Het label staat naast het veld, maar er is geen for/id-koppeling. Klikken op het label doet dan niks, en een schermlezer leest het label niet voor.
  • Keuzerondjes of 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)

Ook de indeling van je pagina leg je vast in de code. Gebieden zoals de navigatie, de hoofdinhoud en een sidebar hebben elk een eigen functie. Leg die functie vast met oriëntatiepunten (landmarks).

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>: informatie naast de hoofdinhoud, zoals gerelateerde links
  • <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: een <section> is alleen een oriëntatiepunt als het een toegankelijke naam heeft via aria-label of aria-labelledby. Wil je dat een <section> een oriëntatiepunt is? Geef het dan 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

De betekenis van tekst moet ook in de code staan 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. Zet korte citaten in de lopende tekst tussen aanhalingstekens. Je kunt ook <q> gebruiken, maar niet elke schermlezer kondigt dat element aan. De browser zet bij <q> zelf aanhalingstekens. Zet ze dan niet ook in de tekst, anders staan ze er dubbel.

Bronvermelding hoort buiten het <blockquote>-element.

<figure>
  <blockquote>
    <p>Het internet is voor iedereen.</p>
  </blockquote>
  <figcaption>Niek</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 in de code 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

  • Maak tekst die eruitziet als een kop in de code op als kop, met <h1> tot en met <h6> of role="heading" met aria-level.
  • Maak lijsten op met <ul>, <ol>, <dl> of role="list" met role="listitem".
  • Gebruik <th> voor tabelkoppen in datatabellen en het scope-attribuut om de richting aan te geven. Complexe tabellen gebruiken headers en id.
  • Gebruik in presentatietabellen geen <th>, <caption>, headers of scope.
  • Koppel elk zichtbaar label in de code aan zijn formulierelement.
  • Groepeer formulierelementen met <fieldset> en <legend> als de losse labels zonder groepslabel niet te begrijpen zijn.
  • Markeer nadruk die betekenis overbrengt met <em>, <strong> of <mark>, of zet de betekenis in tekst.
  • Leg in de code vast welke velden verplicht zijn, met required, aria-required="true" of de aanduiding in het label.
  • Koppel een foutmelding met aria-describedby aan het formulierelement, of noem het label in de foutmelding.
  • Markeer citaten met <blockquote> of <q>.
  • Markeer paginagebieden met <header>, <nav>, <main>, <aside> en <footer> als geen kop of tekst duidelijk maakt welk gebied het is.
  • Gebruik aria-hidden="true", role="presentation" of role="none" niet op content die betekenis overbrengt.

Aanbevolen

  • Gebruik kopniveaus in volgorde en sla niveaus niet over.
  • Gebruik scope ook bij tabelkoppen in de eerste rij of kolom.
  • Gebruik CSS in plaats van een tabel voor lay-out, of geef een presentatietabel role="presentation".
  • Geef tabellen een bijschrift met <caption> of <figcaption>.
  • Groepeer keuzerondjes en selectievakjes altijd met <fieldset> en <legend>.
  • Koppel foutmeldingen altijd met aria-describedby, ook als ze het label noemen.
  • Markeer paginagebieden altijd met <header>, <nav>, <main>, <aside> en <footer>.
  • 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 een vraag over je eigen situatie? Laat het me weten!

Laatst gewijzigd op

Niek Derksen

Ik ben onderzoeker digitale toegankelijkheid bij WCAG.nl, onderdeel van VoorMeerWaarde. In ruim 8 jaar heb ik meer dan 400 toegankelijkheidsonderzoeken uitgevoerd voor overheidsorganisaties en commerciële bedrijven. De inzichten uit die onderzoeken deel ik op deze website.

Vragen of aanvullingen? Laat het weten

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *