Une galerie photo est le projet idéal pour faire travailler ensemble HTML, CSS et JavaScript : une grille qui s'adapte à toutes les tailles d'écran, puis une visionneuse qui affiche les photos en grand, avec les flèches du clavier. Tu vas utiliser deux outils modernes qui simplifient énormément le travail : CSS Grid et l'élément <dialog>. Durée : environ 45 minutes.

Ce que tu vas construire

  • une grille de miniatures qui passe automatiquement de 1 à 4 colonnes selon la largeur de l'écran ;
  • une visionneuse plein écran qui s'ouvre au clic sur une photo ;
  • la navigation avec les boutons ou les flèches du clavier, en boucle ;
  • la fermeture avec la croix, la touche Échap ou un clic sur le fond ;
  • une galerie qui fonctionne même si le JavaScript ne se charge pas.

Crée un dossier avec index.html, style.css, script.js et un sous-dossier images contenant six photos (photo-1.jpg à photo-6.jpg).

1. La structure HTML

Chaque miniature est une image dans un lien qui pointe vers la photo en grand. C'est ce qu'on appelle l'amélioration progressive : sans JavaScript, un clic ouvre simplement la photo ; avec JavaScript, on intercepte le clic pour l'afficher dans la visionneuse.

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Ma galerie</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main>
    <h1>Mes photos de vacances</h1>

    <ul class="gallery">
      <li><a href="images/photo-1.jpg"><img src="images/photo-1.jpg" alt="Coucher de soleil sur la mer" loading="lazy"></a></li>
      <li><a href="images/photo-2.jpg"><img src="images/photo-2.jpg" alt="Sentier dans la forêt" loading="lazy"></a></li>
      <li><a href="images/photo-3.jpg"><img src="images/photo-3.jpg" alt="Village dans la montagne" loading="lazy"></a></li>
      <li><a href="images/photo-4.jpg"><img src="images/photo-4.jpg" alt="Port de pêche au petit matin" loading="lazy"></a></li>
      <li><a href="images/photo-5.jpg"><img src="images/photo-5.jpg" alt="Champ de lavande" loading="lazy"></a></li>
      <li><a href="images/photo-6.jpg"><img src="images/photo-6.jpg" alt="Lac entouré de sommets" loading="lazy"></a></li>
    </ul>
  </main>

  <dialog class="lightbox" aria-label="Photo agrandie">
    <figure>
      <img src="" alt="">
      <figcaption></figcaption>
    </figure>
    <button class="prev" aria-label="Photo précédente">‹</button>
    <button class="next" aria-label="Photo suivante">›</button>
    <button class="close" aria-label="Fermer">×</button>
  </dialog>

  <script src="script.js"></script>
</body>
</html>

Trois détails qui comptent :

  • alt décrit chaque photo : il est lu par les lecteurs d'écran et on le réutilise comme légende ;
  • loading="lazy" ne charge une image que lorsqu'elle approche de l'écran : la page s'affiche plus vite ;
  • <dialog> est une fenêtre modale native : il gère tout seul l'affichage au premier plan, le fond assombri, la touche Échap et le focus clavier.

2. La grille en CSS

body {
  margin: 0;
  font-family: system-ui, sans-serif;
}

main {
  max-width: 72rem;
  margin: 0 auto;
  padding: 1rem;
}

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: 1rem;
  padding: 0;
  list-style: none;
}

.gallery img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 0.5rem;
  transition: transform 0.2s;
}

.gallery a:hover img,
.gallery a:focus-visible img {
  transform: scale(1.03);
}

.lightbox {
  max-width: 90vw;
  padding: 0;
  border: 0;
  background: transparent;
}

.lightbox::backdrop {
  background: rgb(0 0 0 / 0.85);
}

.lightbox img {
  display: block;
  max-width: 90vw;
  max-height: 80vh;
}

.lightbox figcaption {
  padding: 0.5rem 0;
  color: #fff;
  text-align: center;
}

.lightbox button {
  position: fixed;
  top: 50%;
  border: 0;
  background: none;
  color: #fff;
  font-size: 3rem;
  cursor: pointer;
}

.lightbox .prev { left: 1rem; }
.lightbox .next { right: 1rem; }
.lightbox .close { top: 1rem; right: 1rem; }

La ligne clé est grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)) : « crée autant de colonnes d'au moins 14rem que la place le permet, et partage l'espace restant ». Résultat : une grille responsive sans aucune media query. aspect-ratio et object-fit: cover donnent la même forme à toutes les miniatures, même si les photos ont des formats différents.

3. La visionneuse en JavaScript

const links = [...document.querySelectorAll('.gallery a')];
const lightbox = document.querySelector('.lightbox');
const image = lightbox.querySelector('img');
const caption = lightbox.querySelector('figcaption');
let current = 0;

// Affiche la photo n° index ; le modulo fait boucler de la dernière à la première.
function show(index) {
  current = (index + links.length) % links.length;
  const thumbnail = links[current].querySelector('img');
  image.src = links[current].href;
  image.alt = thumbnail.alt;
  caption.textContent = thumbnail.alt;
}

links.forEach((link, index) => {
  link.addEventListener('click', (event) => {
    event.preventDefault(); // Sans JavaScript, le lien ouvre simplement l'image.
    show(index);
    lightbox.showModal();
  });
});

lightbox.querySelector('.prev').addEventListener('click', () => show(current - 1));
lightbox.querySelector('.next').addEventListener('click', () => show(current + 1));
lightbox.querySelector('.close').addEventListener('click', () => lightbox.close());

// Flèches du clavier ; la touche Échap ferme déjà un <dialog> toute seule.
lightbox.addEventListener('keydown', (event) => {
  if (event.key === 'ArrowLeft') show(current - 1);
  if (event.key === 'ArrowRight') show(current + 1);
});

// Un clic sur le fond sombre (en dehors de la photo) ferme aussi la visionneuse.
lightbox.addEventListener('click', (event) => {
  if (event.target === lightbox) lightbox.close();
});

Le cœur du script est la fonction show(index). Le calcul (index + links.length) % links.length fait boucler la navigation : après la dernière photo, on revient à la première, et avant la première, on passe à la dernière.

event.preventDefault() empêche le lien d'ouvrir l'image dans l'onglet, puisque la visionneuse prend le relais. Et showModal() ouvre le <dialog> en mode modal, avec le fond ::backdrop stylé dans le CSS.

4. Tester ta galerie

  • Redimensionne la fenêtre : le nombre de colonnes doit changer tout seul.
  • Clique sur une photo, puis utilise les flèches du clavier : la légende doit suivre.
  • Appuie sur Échap ou clique sur le fond : la visionneuse se ferme.
  • Désactive le JavaScript dans les outils de développement : les liens doivent toujours ouvrir les photos.

Pour aller plus loin

  • Des miniatures légères : génère une petite version de chaque photo pour la grille (photo-1-small.jpg) et garde la grande pour la visionneuse. Ta page sera beaucoup plus rapide.
  • Le balayage sur mobile : écoute les évènements pointerdown et pointerup pour passer à la photo suivante d'un glissement du doigt.
  • Des filtres : ajoute des boutons « Mer », « Montagne »… qui masquent les photos d'une autre catégorie grâce à un attribut data-categorie.

Ta galerie est en ligne ? Partage-la dans la vitrine des lecteurs.