On parle peu d’accessibilité, et pourtant il est utile de garder en tête quelques grands principes, pour que ce que l’on code reste utilisable par celles et ceux de nos utilisateurs qui en ont besoin.

En pratique, une bonne partie tient en quelques réflexes, et Phoenix en fait déjà plusieurs à notre place. Cet article commence par ce qu’il faut garder en tête du RGAA, puis passe en revue six sujets que l’on rencontre dans presque toute application :

  • le contraste des couleurs ;
  • le titre de la page ;
  • les liens qui ouvrent un nouvel onglet ;
  • les images ;
  • les vidéos ;
  • les raccourcis clavier.

Pour chacun, nous verrons ce que demande le référentiel, comment le faire simplement avec Phoenix, et comment le vérifier en quelques secondes.

Le RGAA en quelques lignes

Le RGAA, référentiel général d’amélioration de l’accessibilité, est la méthode française pour vérifier qu’un site est utilisable par les personnes handicapées. La version en vigueur est la 4.1.2, et la version 5 est annoncée pour fin 2026.

Il compte 106 critères, rangés en treize thèmes : images, couleurs, multimédia, formulaires, navigation, et ainsi de suite. Chaque critère est une question précise, comme « chaque image porteuse d’information a-t-elle une alternative textuelle ? », et se décompose en tests que l’on peut vérifier un par un.

Il s’impose aux organismes publics et aux entreprises dont le chiffre d’affaires moyen en France dépasse 250 millions d’euros, d’après le rappel du champ d’application. Depuis le 28 juin 2025, une directive européenne étend l’obligation d’accessibilité aux sites de commerce électronique des PME, hors microentreprises, comme l’explique la fiche de Francenum.

Quatre idées à garder en tête

Le RGAA s’appuie sur les WCAG 2.1, les règles internationales d’accessibilité du web, qui se résument en quatre principes. Ce sont eux qu’il est utile de retenir, plus que les numéros de critères.

Tout ce qui se voit doit pouvoir se percevoir autrement. Une image a un texte qui la remplace, une vidéo a des sous-titres, un texte se distingue assez de son fond pour être lu.

Tout ce qui se fait à la souris doit pouvoir se faire au clavier. Chaque lien, chaque bouton et chaque champ s’atteint avec la touche Tab, et l’on voit toujours où l’on se trouve.

On doit toujours savoir où l’on est et ce qui se passe. La page a un titre clair, rien ne s’ouvre ni ne se déclenche par surprise, et une erreur dit ce qu’il faut corriger.

Le HTML doit dire ce que la page montre. Un lecteur d’écran ne voit pas la page : il lit ce que le navigateur expose de chaque élément, c’est-à-dire son rôle (bouton, lien, champ), son nom et son état. Un vrai <button> avec un texte dedans fait ce travail tout seul, alors qu’une div cliquable ne le fait pas.

Les six sujets qui suivent sont des applications directes de ces quatre idées.

Le contraste : un texte qui se lit

Le RGAA demande un rapport de contraste d’au moins 4,5 pour 1 entre un texte et son fond (critère 3.2). Le seuil descend à 3 pour 1 pour les grands textes, à partir de 24 pixels, ou de 18,5 pixels en gras. Les éléments d’interface, comme la bordure d’un champ ou une icône qui porte une information, demandent eux aussi un contraste suffisant (critère 3.3).

Le rapport se calcule à partir de la luminosité des deux couleurs. Il va de 1 pour 1, deux couleurs identiques, à 21 pour 1, du noir sur du blanc.

Dans une application Phoenix 1.8, les couleurs viennent des thèmes daisyUI définis dans assets/css/app.css, sous forme de variables comme --color-error ou --color-base-100. C’est là qu’il faut agir, une fois pour toute l’application. En mesurant une page générée par mix phx.gen.live, le rouge des messages d’erreur sort à 4,49 pour 1 dans le thème clair et à 2,82 pour 1 dans le thème sombre. Il suffit de l’assombrir dans le premier et de l’éclaircir dans le second :

/* thème sombre */
--color-error: oklch(72% 0.17 20);
--color-error-content: oklch(22% 0.05 20);

/* thème clair */
--color-error: oklch(52% 0.22 20);

Les messages passent alors à 5,77 pour 1 et 5,04 pour 1. Comme --color-error sert aussi de fond aux alertes, le texte posé dessus, --color-error-content, devient foncé dans le thème sombre.

Pour vérifier, le sélecteur de couleur de l’inspecteur de Chrome affiche le rapport de contraste du texte sélectionné, et l’extension axe DevTools signale tous les textes insuffisants d’une page. Il faut penser à vérifier chaque thème, clair et sombre, et les états moins visibles : un texte d’aide, un message d’erreur, un bouton désactivé.

Le titre de page : savoir où l’on est

Chaque page doit avoir un titre (critère 8.5), et ce titre doit être pertinent (critère 8.6), c’est-à-dire identifier la page de façon claire et unique. C’est la première chose qu’annonce un lecteur d’écran, et c’est aussi ce qu’on lit dans l’onglet quand dix pages sont ouvertes.

Phoenix fait déjà l’essentiel. Le composant <.live_title>, dans root.html.heex, met le titre à jour à chaque navigation LiveView, à partir de l’assign page_title. Il reste à remplacer le suffixe généré par le nom de l’application :

<.live_title default="Commandes" suffix=" · MonApp">
  {assigns[:page_title]}
</.live_title>

et à donner à chaque page un titre qui la distingue, du plus précis au plus général :

def mount(%{"id" => id}, _session, socket) do
  commande = Ventes.get_commande!(id)
  {:ok, assign(socket, page_title: "Commande n° #{commande.id}", commande: commande)}
end

Un titre de la forme « Commande n° 42 · MonApp » vaut mieux que « Show Commande · Phoenix Framework », qui est ce que produit le générateur.

Pour vérifier, il suffit d’ouvrir trois pages dans trois onglets : si l’on ne sait pas laquelle est laquelle en lisant les onglets, les titres ne sont pas assez précis.

Les nouveaux onglets : pas de surprise

Le RGAA interdit d’ouvrir une nouvelle fenêtre ou un nouvel onglet sans action de l’utilisateur (critère 13.2). Une fenêtre qui s’ouvre toute seule au chargement de la page, par exemple, désoriente une personne qui ne voit pas l’écran : elle se retrouve ailleurs sans savoir pourquoi, et le bouton « Précédent » ne fonctionne plus.

Un lien avec target="_blank", sur lequel on clique, respecte donc ce critère. Le RGAA 4.1.2 n’impose pas d’en prévenir l’utilisateur, mais c’est une bonne pratique, qui coûte une ligne. Le plus simple est de le dire dans le texte du lien, que tout le monde lit :

<a href="https://hexdocs.pm/phoenix" target="_blank" rel="noopener">
  Documentation de Phoenix <span class="sr-only">(nouvel onglet)</span>
  <.icon name="hero-arrow-top-right-on-square" class="size-4" />
</a>

Le texte caché par sr-only est lu par les lecteurs d’écran, et l’icône le dit aux autres. Pour le reste, je garderais une règle simple : les liens internes s’ouvrent dans le même onglet, et seuls les liens vers d’autres sites, quand on y tient, s’ouvrent ailleurs.

Pour vérifier, on cherche target="_blank" dans le projet, et window.open dans le JavaScript.

Les images : dire ce qu’elles apportent

Le RGAA distingue deux sortes d’images.

Une image qui porte une information doit avoir une alternative textuelle (critère 1.1), et cette alternative doit être pertinente, courte et concise (critère 1.3). Elle dit ce que l’image apporte dans la page, pas ce qu’elle représente : pour le logo qui ramène à l’accueil, « Accueil » vaut mieux que « Logo bleu en forme d’oiseau ».

Une image décorative, au contraire, doit être ignorée par les lecteurs d’écran (critère 1.2). Pour une balise <img>, cela se fait avec un attribut alt vide, et non absent :

<img src={~p"/images/avatar.png"} alt={"Photo de #{@client.nom}"} />
<img src={~p"/images/separateur.svg"} alt="" />

Une image sans attribut alt n’est ni décrite ni ignorée, et elle échoue donc aux deux critères à la fois.

Les icônes de Phoenix, comme <.icon name="hero-trash" />, sont des <span> vides dessinés en CSS. Elles n’ont pas de texte, et les lecteurs d’écran les ignorent : elles sont décoratives par construction. Le piège est le bouton qui ne contient qu’une icône, dont le nom est alors vide. Il faut lui en donner un :

<button phx-click="supprimer" aria-label="Supprimer la commande">
  <.icon name="hero-trash" />
</button>

Une image complexe, comme un graphique ou un schéma, demande en plus une description détaillée, placée à côté de l’image ou accessible par un lien adjacent (critère 1.6). Un tableau des valeurs sous un graphique remplit très bien ce rôle.

Pour les images envoyées par les utilisateurs, le plus simple est de prévoir un champ « description » dans le formulaire d’envoi, et de l’enregistrer avec le fichier pour le placer dans alt. L’article sur les uploads LiveView montre où brancher ce champ.

Pour vérifier, axe signale toute image sans attribut alt et tout bouton sans nom. La pertinence de l’alternative, elle, se juge en lisant la page sans les images.

Les vidéos : sous-titres et transcription

Une vidéo préenregistrée demande, si elle porte une information, des sous-titres synchronisés (critère 4.3) et une transcription textuelle ou une audiodescription (critère 4.1). Les sous-titres servent aux personnes sourdes ou malentendantes. La transcription, qui reprend tout ce qui est dit et ce qui se passe à l’écran, sert aux personnes aveugles et à toutes celles qui préfèrent lire.

En HTML, les sous-titres passent par une balise <track>, et le RGAA demande qu’elle porte l’attribut kind="captions". La transcription peut se placer juste en dessous, dans un bloc qui se déplie :

<video controls src={~p"/videos/demo.mp4"}>
  <track kind="captions" src={~p"/videos/demo.fr.vtt"} srclang="fr" label="Français" default />
</video>

<details>
  <summary>Transcription de la vidéo</summary>
  <p>…</p>
</details>

Le fichier .vtt est un simple fichier texte, qui associe chaque phrase à son horodatage. La plupart des outils de montage et des plateformes vidéo savent l’exporter.

Il faut aussi que les commandes de la vidéo s’utilisent au clavier (critère 4.11), et qu’aucun son ne se déclenche tout seul plus de trois secondes sans pouvoir être arrêté (critère 4.10). L’attribut controls affiche les commandes du navigateur, ce qui est plus sûr que de les reconstruire, et l’on évite autoplay sur une vidéo sonore.

Pour vérifier, on regarde la vidéo une fois sans le son, puis on la pilote au clavier : lecture, pause, volume et sous-titres.

Les raccourcis clavier : utiles, mais jamais sur une seule touche

Un raccourci qui tient en une seule touche, une lettre, un chiffre ou un signe de ponctuation, gêne les personnes qui pilotent leur ordinateur à la voix : un mot dicté peut déclencher une série d’actions. Le RGAA demande donc qu’un tel raccourci puisse être désactivé, ou reconfiguré avec une touche de modification comme Ctrl, ou qu’il ne fonctionne que lorsque le composant concerné a le focus (critère 12.10).

En LiveView, un raccourci global s’écrit avec phx-window-keydown et phx-key. Par défaut, l’événement envoyé au serveur ne dit pas si Ctrl était enfoncé. On l’ajoute dans app.js, avec l’option metadata du LiveSocket :

const liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken},
  hooks: {...colocatedHooks},
  // transmet au serveur l'état des touches de modification avec chaque touche pressée
  metadata: {
    keydown: (e) => ({ctrlKey: e.ctrlKey, metaKey: e.metaKey, altKey: e.altKey}),
  },
})

Le raccourci peut alors exiger Ctrl+K, ou Cmd+K sur Mac, et ignorer la touche K seule :

<div phx-window-keydown="raccourci" phx-key="k"></div>
def handle_event("raccourci", %{"key" => "k"} = touche, socket) do
  if touche["ctrlKey"] or touche["metaKey"] do
    {:noreply, push_navigate(socket, to: ~p"/commandes/new")}
  else
    {:noreply, socket}
  end
end

Dans le navigateur, la touche K seule ne fait rien, et Ctrl+K ouvre le formulaire de création. La touche Échap, elle, n’est ni une lettre, ni un chiffre, ni un signe : on peut l’utiliser seule pour fermer une fenêtre, comme dans l’article sur Phoenix.LiveView.JS.

Un raccourci reste un bonus. Tout ce qu’il permet doit aussi être possible avec un lien ou un bouton, atteignable avec la touche Tab (critère 7.3).

Pour vérifier, on cherche phx-window-keydown et addEventListener("keydown" dans le projet, et l’on regarde, pour chacun, si une touche seule suffit à déclencher l’action.

Par où commencer

Le plus rentable est de parcourir chaque page au clavier, touche Tab après touche Tab, sans toucher la souris. On voit tout de suite un élément qu’on ne peut pas atteindre, un focus qui disparaît, ou une action réservée à la souris.

Ensuite, l’extension axe DevTools passe la page au crible en quelques secondes. Elle trouve les oublis mécaniques : image sans alt, bouton sans nom, contraste insuffisant. Elle ne juge pas de la pertinence d’un texte, et elle ne voit pas ce qu’on découvre au clavier.

Enfin, ces vérifications ne remplacent pas un audit des 106 critères, nécessaire pour déclarer un site conforme. Elles évitent simplement que cet audit relève les mêmes erreurs sur chaque page.

Pour aller plus loin côté code, le notebook qui accompagne cet article montre comment vérifier une partie de ces règles dans un test LiveView, sur l’exemple d’un formulaire dont les erreurs sont reliées à leur champ : RGAA et LiveView : ce que le navigateur expose, et ce qu’un test peut vérifier.

À retenir

  • Le RGAA 4.1.2 compte 106 critères en treize thèmes, mais quatre idées suffisent pour commencer : percevoir autrement, tout faire au clavier, savoir où l’on est, et un HTML qui dit ce que la page montre.
  • Contraste : 4,5 pour 1 pour un texte courant, à régler une fois dans les variables du thème, et dans chaque thème.
  • Titre de page : un page_title précis dans chaque LiveView, et le nom de l’application en suffixe.
  • Nouvel onglet : jamais sans clic de l’utilisateur, et de préférence annoncé dans le texte du lien.
  • Images : une alternative courte qui dit ce que l’image apporte, alt="" pour une image décorative, et un nom pour tout bouton réduit à une icône.
  • Vidéos : des sous-titres avec <track kind="captions">, une transcription, et les commandes du navigateur.
  • Raccourcis : jamais une seule touche, sauf si le composant a le focus ; Ctrl+K se met en place avec l’option metadata du LiveSocket.