Le format SVG peut paraître parfois un peu intimidant, et l'associer à des transitions ou des animations CSS semble encore plus audacieux pour bon nombre de personnes.
Cependant, dans certains cas, l'alchimie entre SVG et CSS est aussi bénéfique qu'extrêmement simple à mettre en oeuvre. Dans ce tutoriel, nous allons suivre étape par étape comment animer un bouton burger simple avec SVG et CSS.
Quels outils ?
La liste des outils nécessaires pour atteindre nos objectifs est particulièrement réduite puisqu'un simple éditeur de code fait le job (n'importe lequel fait l'affaire, Visual Studio Code étant mon choix personnel).
Pour aller plus loin, et en guise de bonus, on peut également piocher :
- Un éditeur SVG en ligne (parce que ça peut toujours servir)
- Des recommendations concernant l'accessibilité des SVG (au hasard les Guidelines Alsacréations)
- Un éditeur de courbes de Bezier (pour des animations originales)
SVG c'est quoi ?
En trois mots, voici comment résumer SVG :
- SVG est un format graphique vectoriel (composé de tracés et de courbes)
- Il est développé et maintenu depuis 1999 par le W3C (standard officiel, open source)
- Il est conçu en XML (compatible HTML) (on peut le créer et le lire avec un simple éditeur de texte)
1. Produire le burger bouton en SVG
Si l'on y regarde de plus près, une "icône Burger" c'est bêtement trois rectangles horizontaux espacés et avec des coins arrondis.
Notre éditeur de code préféré est amplement suffisant pour s'aquitter de la tâche de dessiner des rectangles : on va tout d'abord dessiner un élément SVG vide avec une fenêtre de "100 x 100". C'est une dimension purement indicative car tout est proportionnel et adaptable en SVG.
<svg class="burger-icon" viewBox="0 0 100 100">
</svg>
.burger-icon {
width: 200px; height: 200px; /* taille du SVG */
border: 2px dotted #ddd; /* bordure = simple repère */
}
Le tracé de notre premier rectangle est un jeu d'enfant aussi : l'élément SVG rect
est fait pour ça, attribuons-lui des coordonnées (x=0
et y=0
) ainsi qu'une largeur de "100" et une hauteur de "20".
<svg class="burger-icon" viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="20" />
</svg>
Vous aurez compris qu'à partir d'un premier rectangle, il n'est pas difficile de produire les deux suivants. Et voilà !
<svg class="burger-icon" viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="20" />
<rect x="0" y="40" width="100" height="20" />
<rect x="0" y="80" width="100" height="20" />
</svg>
Pour ce qui est des coins arrondis, là aussi SVG a tout prévu sous la forme de l'attribut rx
, à qui une valeur de "5" semble tout à fait parfaite.
<svg class="burger-icon" viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="20" rx="5" />
<rect x="0" y="40" width="100" height="20" rx="5" />
<rect x="0" y="80" width="100" height="20" rx="5" />
</svg>
Le résultat est bluffant et on se rend compte de la puissance insoupçonnée d'un éditeur de code. Plus sérieusement, ce n'était vraiment pas compliqué, non ?
Par contre, ce qui est vraiment dommage c'est de répéter les mêmes choses plusieurs fois…
Mais justement, il se trouve que… la plupart des attributs SVG existent également sous forme de propriétés CSS ! Voici par conséquent comment nous allons pouvoir améliorer notre code actuel :
<svg class="burger-icon" viewBox="0 0 100 100">
<rect class="rect-1" />
<rect class="rect-2" />
<rect class="rect-3" />
</svg>
rect {
x: 0;
rx: 5px;
width: 100px;
height: 20px;
}
.rect-1 {
y: 0;
}
.rect-2 {
y: 40px;
}
.rect-3 {
y: 80px;
}
Autre avantage loin d'être anodin, ces propriétés CSS-SVG ont la bonne idée d'être animables : on peut par exemple effectuer une transition
sur la propriété… y
!
rect {
...
transition: y 1s;
}
.rect-1:hover {
y: 15px;
}
2. Préparer le SVG et le rendre accessible
Nous allons à présent nous atteler à transformer notre icône SVG en un véritable "bouton Burger", fonctionnel et accessible.
Pour ce faire, on commence par placer le SVG dans un <button>
qui sera l'élément interactif au clic / touch et qui déclenchera l'animation.
<button class="burger-button">
<svg class="burger-icon" viewBox="0 0 100 100">
<rect class="rect-1" />
<rect class="rect-2" />
<rect class="rect-3" />
</svg>
</button>
Notre icône SVG est considérée comme purement décorative, car c'est le bouton qui portera l'information. Nous veillons à lui appliquer les attributs suivants :
- Un attribut
aria-hidden="true"
- Un attribut
focusable="false"
pour éviter de naviguer au sein du SVG. - Aucun élément
<title>
ni<desc>
ni d'attributtitle
,aria-label
,aria-labelledby
, nirole="img"
...
<svg class="burger-icon" aria-hidden="true" focusable="false" viewBox="0 0 100 100">
</svg>
...
Le bouton, quant à lui, nécessite les éléments suivants :
- Un nom accessible (via
aria-label
ou un texte masqué à la ".sr-only") - En option, et selon les cas de figure, un attribut
aria-controls
pour lier à la cible et un attributaria-expanded
pour signaler l'état du bouton. Dans notre cas, ce n'est pas nécessaire.
<button class="burger-button" aria-label="Menu" data-expanded="false">
...
</button>
Voici le script JavaScript destiné à gérer l'interaction et la mise à jour des attributs data-
, et déclencher l'animation de l'icône :
(function () {
function toggleNav() {
// Define targets
const button = document.querySelector('.burger-button');
const target = document.querySelector('#navigation');
button.addEventListener('click', () => {
const currentState = target.getAttribute("data-state");
if (!currentState || currentState === "closed") {
target.setAttribute("data-state", "opened");
button.setAttribute("data-expanded", "true");
} else {
target.setAttribute("data-state", "closed");
button.setAttribute("data-expanded", "false");
}
});
} // end toggleNav()
toggleNav();
}());
aria-expanded
ou non ?
L'utilisation de aria-expanded
sur un bouton n'est pas systématique, dans le cas d'un menu tout dépend de comment celui-ci va s'ouvrir :
- S'il s'agit d'une modale par exemple (donc si tout le reste doit devenir
inert
), alors le bouton reste un bouton simple, pas d'aria-expanded
. - S'il s'agit d'un menu déroulant, alors oui il faudra un attribut
aria-expanded.
.
3. Les étapes de l'animation
Pour être très précis, nous n'allons pas employer une "animation" pour nos effets, mais une combinaison de trois "transitions", qui se révèleront amplement suffisantes pour notre besoin.
Voici le scénario étape par étape qui doit se réaliser :
- L'action de clic ou de touch sur l'élément
button
doit déclencher une série de trois transitions; - La transition 1 consiste en un déplacement vertical de
.rect-1
et.rect-3
qui se rejoignent au centre du SVG; - La transition 2 consiste à faire disparaître
.rect-2
qui traîne dans nos pattes. En terme de timing, cette transition doit se dérouler en même temps que la transition 1; - La transition 3 se compose d'une rotation de 45 degrés de
.rect-1
et.rect-3
et doit de déclencher juste après les transitions précédentes).
Transition 1 et 2 : "translate" et "opacity"
La propriété transition
est appliquée sur l'élément à l'état initial (hors événement) afin d'assurer une transition au retour lorsque l'événement est quitté.
/* transition sur la propriété y et opacity, durée 0.3s */
rect {
transition:
y 0.3s,
opacity 0.3s;
}
/* coordonnées y initiales */
.rect-1 {
y: 0;
}
.rect-2 {
y: 40px;
}
.rect-3 {
y: 80px;
}
Au clic, le bouton passe en data-expanded "true"
et on déplace verticalement deux rectangles au centre et on masque le 3e rectangle central.
[data-expanded="true"] .rect-1 {
y: 40px;
}
[data-expanded="true"] .rect-2 {
opacity: 0;
}
[data-expanded="true"] .rect-3 {
y: 40px;
}
Transition 3 : "rotate"
Aux deux transitions précédentes, on ajoute une transition sur la propriété rotate
sans oublier de la faire débuter après un léger délai.
/* on attend un delai de 0.3s avant de commencer rotate */
rect {
transition:
y 0.3s,
opacity 0.3s,
rotate 0.3s 0.3s;
}
Au clic, les trois transitions se déclenchent.
[data-expanded="true"] .rect-1 {
y: 40px;
rotate: 45deg;
}
[data-expanded="true"] .rect-2 {
opacity: 0;
}
[data-expanded="true"] .rect-3 {
y: 40px;
rotate: -45deg;
}
⚠️ J'imagine que cela ne vous a pas échappé : tout se passe très bien à l'aller, mais malheureusement pas au retour. L'explication provient du fait que la transition se déroule dans le sens inverse au retour et que la rotation se déclenche trop tôt. Il va nous falloir une transition différente à l'aller et au retour et gérer des délais différents entre la transition et la rotation.
/* transition au retour (quand on perd le clic) */
/* on attend un delai de 0.3s avant de commencer y */
rect {
transition:
y 0.3s 0.3s,
opacity 0.3s,
rotate 0.3s;
}
/* transition à l'aller (quand on clique) */
/* on attend un delai de 0.3s avant de commencer rotate */
[data-expanded="true"] rect {
transition:
y 0.3s,
opacity 0.3s,
rotate 0.3s 0.3s;
}
Grâce à cette adaptation subtile, notre effet fonctionne parfaitement à l'aller et au retour lors de l'interaction.
Pour finir en beauté, le truc en plus consiste en une petite accélération sous forme de cubic-bezier
pour un effet de "rebond".
[data-expanded="true"] rect {
transition:
y 0.3s,
opacity 0.3s,
rotate 0.3s 0.3s cubic-bezier(.55,-0.65,0,2.32);
}
CSS final
Voici les styles CSS complets de ce tutoriel.
Notez qu'ils prennent en compte les préférences utilisateur grâce au media query prefers-reduced-motion
: si la personne a choisi dans ses réglages système de réduire les animations, celles-ci ne seront tout simplement pas déclenchées.
Pour voir le résultat et aller plus loin, une petite collection CodePen de boutons burger animés a été rassemblée à cette adresse : https://codepen.io/collection/VYqwJK
.rect-1 {
y: 0;
}
.rect-2 {
y: 40px;
}
.rect-3 {
y: 80px;
}
[data-expanded="true"] .rect-1 {
y: 40px;
rotate: 45deg;
}
[data-expanded="true"] .rect-2 {
opacity: 0;
}
[data-expanded="true"] .rect-3 {
y: 40px;
rotate: -45deg;
}
/* transitions si acceptées */
@media (prefers-reduced-motion: no-preference) {
rect {
transition:
y 0.3s 0.3s,
opacity 0.3s,
rotate 0.3s;
}
[data-expanded="true"] rect {
transition:
y 0.3s,
opacity 0.3s,
rotate 0.3s 0.3s cubic-bezier(.55,-0.65,0,2.32);
}
}
Commentaires
Salut,
Alors du coup je n'ai pas compris pourquoi tu as préféré les attributs data sur les attributs aria dans cette situation.
Concrètement pourquoi préférer un `data-expanded` à un `aria-expanded` ? De même pour le panneau, il me semble que l'on pourrait utiliser avec pertinence un `aria-hidden="false"` en lieu et place d'un `data-state="opened"`. Ce qui bien sûr oblige ensuite à lier le tout avec les attributs `aria-controls`pour le burger et `aria-labelledby` pour le panneau. Mais pour l'accessibilité, au moins sur le papier, il me semble que ce serait top, non ? Où alors il y a quelque chose qui m'échappe ?
Pour le CSS il n'y aurait quasiment rien à changer, au lieu de `[data-expanded="true"] rect {}` on aurait `[aria-expanded="true"] rect {}`, etc.
Je viens d'updater mon site avec mes propres suggestions pour illustrer mon propos sur les aria (cf. le lien sur mon profil). Voici le code JavaScript (vous noterez notamment l'ajout d'une classe active sur les tags html et body, pour leur appliquer notamment un overflow: clip, ainsi que l'ajout d'un attribut inert sur tous les éléments qui ne font pas partie de la navigation principale.) :
const mainMenu = (() => {
const button = document.querySelector('.cmd-nav'),
subNav = document.querySelector('.sub-nav'),
content = document.querySelectorAll('body > :not(.nav')
button.ariaExpanded = 'false'
subNav.ariaHidden = 'true'
button.addEventListener('click', () => {
document.documentElement.classList.toggle('active')
document.body.classList.toggle('active')
button.ariaExpanded = button.ariaExpanded === 'true' ? 'false' : 'true'
subNav.ariaHidden = subNav.ariaHidden === 'true' ? 'false' : 'true'
content.forEach(e => (e.hasAttribute('inert') ? e.removeAttribute('inert') : e.setAttribute('inert', '')))
})
})()
@Olivier C :
En fait, l'utilisation de `aria-expanded` sur un bouton n'est pas systématique : dans le cas d'un menu tout dépend de comment celui-ci va s'ouvrir.
- S'il s'agit d'une modale par exemple (donc si tout le reste doit devenir `inert`), alors le bouton reste un bouton simple, pas d'`aria-expanded`.
- S'il s'agit d'un menu déroulant, alors oui il faudra un attribut `aria-expanded.`.
Source : une collègue experte en Accessibilité, sur les infos fournies par les design patterns : https://www.w3.org/WAI/ARIA/apg/example-index/#examples_by_props_label
Je ne sais trop quoi penser... parce que justement je m'appuie sur les documents du W3C dont tu parles (j'ai pris cette habitude depuis 2-3 ans) et je viens de vérifier : quelque-soit le pattern que je consulte l'utilisation de `aria-expanded` sur le bouton reste une constante (d'ailleurs tu as vu que le burger de ton propre forum utilise `aria-expanded` ? je viens de m'en apercevoir). Je m'appuie notamment sur cet exemple :
https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/examples/menu-button-links/
Pour moi ce point me semble acquit, de mon côté la question actuelle est plutôt de savoir si je dois ajouter ou non un attribut `aria-haspopup`...
L'argument pour l'utilisation du menu en lien avec `inert` me parait bonne, mais je serais curieux d'auditer les sites utilisant cet attribut au déclenchement de leur menu, ça ne doit pas courir les rues. De mon côté je le fais déjà de toute façon, comme je le disais plus haut, mais l'utilisation de l'un n'empêche pas l'autre, alors pourquoi s'en priver ?
Les motifs de conception ARIA ne sont pas toujours à utiliser tels quels et sont à utiliser dans des cas précis (après avoir vérifié leur compatibilité).
C'est plus une référence théorique, mais dans la pratique... c'est plus compliqué : leur implémentation est vite foireuse, et souvent il vaut mieux rester simple.
Ce qui est acquis est que si le bouton burger ouvre le menu dans une modale, alors c'est un bouton simple.
Je me suis posé la question d'une animation de mon menu burger.
Mais dans mon cas il n'est présent qu'en mode mobile. Du coup l'animation se ferait au moment où on le touche, non visible sous le doigt !! Donc inutile...
@kerlutinoec : Je ne suis pas sûr d'avoir compris, puisque habituellement ces boutons burgers animés sont appliqués sur mobile (le bouton burger de ce site Alsacréations, par exemple, est bien animé sur mobile)
Peut-être parle-t-on ici d'une animation sur les pseudo-classes :focus et :hover.
Une animation sur ces pseudo-classes peut être sympa car les menus aux items conséquents (et qui ont leur propre breakpoint) peuvent très bien apparaître sur une fenêtre de taille moyenne. L'animation invitera alors à l'action, surtout pour un utilisateur desktop qui s'attend à ce qu'un élément potentiellement cliquable soit signalé de cette manière.
Perso c'est ce que je fais.
@Raphael : Bein du coup l'animation aura lieu au moment où ton doigt est encore dessus et ne sera quasiment pas vue non ?
@kerlutinoec : tu as testé ?
.3s c'est pas mal déjà. Moi je limite à .2s et on voit très bien l'animation sous le doigt. C'est peut-être aussi une question de taille de l'icône.