Leçon 2/12 13 min

Projet 2 : un générateur de palette avec l'IA

TD corrigé : un générateur de palette de couleurs construit avec l'IA. Les vrais prompts, et la relecture qui transforme une div cliquable en bouton accessible et garantit un texte lisible sur chaque couleur.

FR EN

Le projet : une palette qu'on peut piquer en un clic

Deuxième projet, on monte d'un cran. Le projet 1 affichait du texte ; ici, on va vraiment fabriquer des éléments et interagir avec. Au menu : un générateur de palette de couleurs. Cinq couleurs côte à côte, un bouton pour en tirer de nouvelles, et un clic sur une couleur copie son code hex dans le presse-papier. C'est typiquement le petit outil qu'on ouvre pour de vrai quand on démarre une maquette et qu'on cherche une ambiance.

Pourquoi ce projet maintenant ? Parce qu'il introduit une chose nouvelle : on génère du HTML à la volée (les cinq pastilles), on le rend cliquable, et on lit une donnée dessus. Et c'est précisément sur ce terrain que l'IA va prendre deux raccourcis « qui marchent » à l'écran… mais qui laissent de côté tous ceux qui n'ont pas une souris et de bons yeux. On reprend la même boucle qu'au projet 1 : on code vite, on relit lentement.

Toujours la même contrainte : un seul fichier HTML, zéro dépendance. Tu pourras le télécharger et l'ouvrir tel quel à la fin.

Prompt 1 : poser le cadre

Comme avant, on commence par cadrer la demande au lieu de lâcher une idée floue. On précise le rendu (cinq couleurs côte à côte), l'interaction (clic = copie), et ce qu'on veut voir (le code hex sur chaque couleur) :

Crée une mini-app web autonome, un seul fichier HTML (HTML + CSS + JS, zéro dépendance, pas de framework). Objectif : afficher 5 couleurs aléatoires côte à côte, avec un bouton « Nouvelle palette ». Quand on clique une couleur, on copie son code hex. Affiche le code hex sur chaque couleur. Bilingue FR/EN. Code simple et lisible.

L'IA livre une version qui marche. Pour rendre chaque couleur cliquable, elle fait au plus court : une <div> avec un onclick, et du texte blanc dessus.

function render() {
  palette.innerHTML = '';
  colors.forEach(function (hex) {
    const d = document.createElement('div');
    d.className = 'swatch';
    d.style.background = hex;
    d.style.color = 'white';          // ⚠️ texte toujours blanc
    d.textContent = hex;
    d.onclick = function () { copy(hex); };  // ⚠️ div cliquable
    palette.appendChild(d);
  });
}

À l'écran, ça a l'air parfait : les couleurs s'affichent, le clic copie. Sauf que « à l'écran » et « à la souris » ne sont pas tout le monde. Deux problèmes dorment dans ces quelques lignes — et le second est sournois parce qu'il n'apparaît qu'une fois sur dix.

Ma relecture humaine : 3 trucs que l'IA a laissés passer

Sur le projet 1, le réflexe-clé était la sécurité (injection) et le contraste. Ici, c'est surtout l'accessibilité qui va parler — comment quelqu'un qui n'utilise pas de souris, ou qui ne voit pas l'écran, peut quand même se servir de l'outil. Voici les trois points repris, et pourquoi.

1. Une <div> cliquable n'est pas un bouton

Mettre un onclick sur une <div>, ça marche… à la souris, et seulement à la souris. Pour le reste du monde, cette div est invisible : on ne peut pas l'atteindre avec la touche Tab, elle n'annonce aucun rôle à un lecteur d'écran, et appuyer sur Entrée ou Espace ne fait rien. Un <button>, lui, apporte tout ça gratuitement : le focus clavier, le rôle « bouton », l'activation au clavier. La règle est simple et tient pour toujours : tout ce qui se clique et déclenche une action est un <button>, pas une div déguisée.

2. Le texte blanc disparaît sur les couleurs claires

C'est le piège vicieux. color: white écrit en dur, c'est nickel sur un bleu nuit… et totalement illisible sur un jaune pâle ou un beige. Or les couleurs sont tirées au hasard : impossible de deviner à l'avance. Et comme au projet 1, rien ne te prévient — il faut tomber sur le mauvais tirage pour le voir. La bonne réponse n'est pas de tâtonner, c'est de calculer : on mesure la luminance de chaque couleur (la formule officielle du WCAG, celle de l'accessibilité web) et on met le texte en noir sur les couleurs claires, en blanc sur les foncées. Quelle que soit la couleur tirée, le code hex reste lisible.

3. La copie doit être annoncée

Quand on copie un code, un petit « Copié : #xxxxxx » s'affiche. Visible pour qui regarde l'écran, muet pour qui utilise un lecteur d'écran. On le place donc dans une zone aria-live="polite", qui est relue dès que son contenu change. Et tant qu'on y est, chaque bouton de couleur reçoit un aria-label parlant (« Copier la couleur #xxxxxx ») au lieu de laisser le lecteur d'écran ânonner six caractères hexadécimaux hors contexte.

Prompt 2 : durcir après relecture

Trois constats, trois corrections. On repasse la main à l'IA avec une consigne précise pour chacune :

Trois corrections : (1) remplace les div cliquables par de vrais <button> (focus clavier + activation Entrée/Espace). (2) Ne mets plus le texte en blanc en dur : calcule la luminance relative (WCAG) de chaque couleur et choisis un texte noir ou blanc pour garantir le contraste. (3) Ajoute aria-live="polite" sur le statut et un aria-label explicite sur chaque bouton de couleur.
// Luminance relative (WCAG) : noir sur les couleurs claires, blanc sur les foncées
function readableTextColor(hex) {
  var r = parseInt(hex.slice(1,3),16)/255,
      g = parseInt(hex.slice(3,5),16)/255,
      b = parseInt(hex.slice(5,7),16)/255;
  function lin(c){ return c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4); }
  var L = 0.2126*lin(r) + 0.7152*lin(g) + 0.0722*lin(b);
  return L > 0.179 ? '#111111' : '#ffffff';
}

// Un vrai bouton : focusable et activable au clavier
var b = document.createElement('button');
b.type = 'button';
b.style.color = readableTextColor(hex);
b.setAttribute('aria-label', 'Copier la couleur ' + hex);

Tu remarques le fil rouge avec le projet 1 ? Le contraste, encore. Là où on avait choisi des dégradés sûrs, ici on le calcule. Deux façons de régler le même réflexe : ne jamais laisser le hasard décider de la lisibilité.

Héberger et tester

Comme le projet 1, c'est un fichier statique : on le dépose sur le serveur, au même endroit que le site, et c'est en ligne. Côté tests, on garde les bons réflexes, plus un spécifique à ce projet :

  • Naviguer au clavier seul (touche Tab) : on doit pouvoir atteindre chaque couleur et la copier avec Entrée. C'est le test qui prouve que les <button> ont servi à quelque chose.
  • Cliquer « Nouvelle palette » une vingtaine de fois : sur chaque couleur, le code hex doit rester lisible, clair comme foncé.
  • Vérifier la copie (coller ailleurs), la bascule FR / EN, et la console (zéro erreur).

Le rendu final

Le projet terminé, en vrai, dans la page :

Ouvrir le projet en plein écran

Le code complet (et téléchargeable)

Le fichier entier, exactement celui qui tourne au-dessus. Télécharge-le, ouvre-le, lis-le.

Télécharger le code (.html · 243 lignes)

Voir le code complet
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Générateur de palette de couleurs</title>
<meta name="description" content="Génère une palette de couleurs et copie un code hex en un clic. Mini-projet construit avec l'IA — web-developpeur.com">
<meta name="robots" content="noindex, follow">
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">

<!--
  ============================================================================
  GÉNÉRATEUR DE PALETTE — un seul fichier : HTML + CSS + JS, zéro dépendance.
    1. <style>  : l'apparence (les 5 pastilles, les boutons, le responsive).
    2. <body>   : la structure (en-tête + zone palette + bouton).
    3. <script> : la logique (tirer des couleurs, choisir noir/blanc, copier).
  Point clé du projet : le texte de chaque couleur passe en noir OU blanc
  automatiquement, selon la luminosité du fond, pour rester toujours lisible.
  ============================================================================
-->

<style>
  :root { --ink:#1a1d24; --muted:#5a6270; --accent:#267d42; --border:#e2e6ea; }
  * { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; }
  body {
    font-family: 'Segoe UI', system-ui, -apple-system, Roboto, Helvetica, Arial, sans-serif;
    background: #f4f6f8; color: var(--ink); min-height: 100vh;
    display: flex; flex-direction: column; align-items: center;
    padding: 32px 18px 48px; line-height: 1.5;
  }
  .app { width: 100%; max-width: 680px; }
  header { text-align: center; margin-bottom: 22px; }
  h1 { font-size: 1.5rem; margin: 0 0 6px; letter-spacing: -0.01em; }
  .sub { color: var(--muted); font-size: 0.95rem; margin: 0 0 16px; }

  .lang-switch { display: inline-flex; border: 1.5px solid var(--border); border-radius: 999px; overflow: hidden; background: #fff; }
  .lang-btn { border: 0; background: transparent; padding: 7px 18px; font: inherit; font-weight: 700; font-size: 0.8rem; color: var(--muted); cursor: pointer; }
  .lang-btn[aria-pressed="true"] { background: var(--accent); color: #fff; }
  .lang-btn:focus-visible { outline: 3px solid rgba(38,125,66,0.4); outline-offset: 2px; }

  /* La palette = 5 pastilles côte à côte (flex). Chaque pastille est un <button>. */
  .palette { display: flex; gap: 0; border-radius: 16px; overflow: hidden; box-shadow: 0 16px 38px rgba(15,25,35,0.16); }
  .swatch {
    flex: 1 1 0; min-height: 220px; border: 0; cursor: pointer;
    display: flex; align-items: flex-end; justify-content: center;
    padding-bottom: 18px; font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-weight: 700; font-size: 0.95rem; letter-spacing: 0.02em;
    transition: flex-grow 0.2s ease;
  }
  .swatch:hover { flex-grow: 1.25; }                              /* la pastille survolée s'élargit un peu */
  .swatch:focus-visible { outline: 3px solid #111; outline-offset: -3px; }   /* contour clavier */

  .actions { display: flex; gap: 12px; margin-top: 22px; flex-wrap: wrap; justify-content: center; }
  .btn { min-height: 50px; padding: 0 26px; border-radius: 12px; border: 1.5px solid transparent; font: inherit; font-weight: 700; font-size: 0.98rem; cursor: pointer; transition: transform 0.12s ease, box-shadow 0.2s ease, background 0.2s ease; }
  .btn-primary { background: var(--accent); color: #fff; box-shadow: 0 6px 16px rgba(38,125,66,0.28); }
  .btn-primary:hover { background: #1f6a37; transform: translateY(-1px); }
  .btn:focus-visible { outline: 3px solid rgba(38,125,66,0.4); outline-offset: 2px; }

  .hint { text-align: center; color: var(--muted); font-size: 0.85rem; margin-top: 14px; }
  .status { text-align: center; min-height: 20px; margin-top: 6px; font-size: 0.85rem; font-weight: 600; color: var(--accent); }
  .credit { text-align: center; color: var(--muted); font-size: 0.78rem; margin-top: 28px; }
  .credit a { color: var(--accent); }

  /* Sur petit écran : les pastilles s'empilent verticalement. */
  @media (max-width: 560px) {
    .palette { flex-direction: column; }
    .swatch { min-height: 60px; align-items: center; padding-bottom: 0; }
    .swatch:hover { flex-grow: 1; }
  }
  @media (prefers-reduced-motion: reduce) { .swatch, .btn { transition: none; } }
</style>
</head>
<body>

<main class="app">
  <header>
    <h1 data-i18n="title">Palette de couleurs</h1>
    <p class="sub" data-i18n="sub">Génère une palette, clique une couleur pour copier son code hex.</p>
    <div class="lang-switch" role="group" aria-label="Langue / Language">
      <button class="lang-btn" data-lang-btn="fr" aria-pressed="true" type="button">FR</button>
      <button class="lang-btn" data-lang-btn="en" aria-pressed="false" type="button">EN</button>
    </div>
  </header>

  <!-- Cette zone est vide au départ : le JS y insère les 5 pastilles. -->
  <div class="palette" id="palette" role="group" aria-label="Palette"></div>

  <div class="actions">
    <button class="btn btn-primary" id="gen" type="button" data-i18n="gen">Nouvelle palette</button>
  </div>

  <p class="hint" data-i18n="hint">Clique une couleur pour copier son code hex.</p>
  <p class="status" id="status" role="status" aria-live="polite"></p>

  <p class="credit" data-i18n="credit">Mini-projet du cours <a href="/apprendre/projets/">Projets appliqués</a> — web-developpeur.com</p>
</main>

<script>
(function () {
  'use strict';

  /* ---------------------------------------------------------------------------
     1. LES TEXTES DE L'INTERFACE (traduits FR / EN).
     --------------------------------------------------------------------------- */
  var UI = {
    fr: {
      title: "Palette de couleurs",
      sub: "Génère une palette, clique une couleur pour copier son code hex.",
      gen: "Nouvelle palette",
      hint: "Clique une couleur pour copier son code hex.",
      credit: 'Mini-projet du cours <a href="/apprendre/projets/">Projets appliqués</a> — web-developpeur.com',
      copied: "Copié : ",
      copyfail: "Copie impossible — sélectionne le code à la main.",
      copyLabel: "Copier la couleur "
    },
    en: {
      title: "Color palette",
      sub: "Generate a palette, click a color to copy its hex code.",
      gen: "New palette",
      hint: "Click a color to copy its hex code.",
      credit: 'A mini-project from the <a href="/apprendre/projets/">Applied projects</a> course — web-developpeur.com',
      copied: "Copied: ",
      copyfail: "Couldn't copy — select the code manually.",
      copyLabel: "Copy color "
    }
  };

  /* ---------------------------------------------------------------------------
     2. LES ÉLÉMENTS DE LA PAGE + L'ÉTAT (la langue, les couleurs affichées).
     --------------------------------------------------------------------------- */
  var paletteEl = document.getElementById('palette');
  var statusEl = document.getElementById('status');
  var genBtn = document.getElementById('gen');

  var lang = 'fr';
  try { lang = localStorage.getItem('palette-lang') || 'fr'; } catch (e) {}
  if (lang !== 'fr' && lang !== 'en') lang = 'fr';

  var colors = [];   // les 5 codes hex de la palette en cours

  /* ---------------------------------------------------------------------------
     3. LES OUTILS COULEUR — tirer une couleur, choisir un texte lisible.
     --------------------------------------------------------------------------- */

  // Tire une couleur au hasard et la renvoie au format "#rrggbb".
  function randomHex() {
    var n = Math.floor(Math.random() * 0x1000000);     // un nombre entre 0 et 16 777 215
    return '#' + ('000000' + n.toString(16)).slice(-6); // converti en hexa, complété à 6 caractères
  }

  // Renvoie '#111111' (noir) ou '#ffffff' (blanc) selon la LUMINOSITÉ du fond,
  // pour que le texte reste lisible quelle que soit la couleur. (Formule officielle WCAG.)
  function readableTextColor(hex) {
    // 1) on sépare les composantes rouge / vert / bleu, ramenées entre 0 et 1
    var r = parseInt(hex.slice(1, 3), 16) / 255;
    var g = parseInt(hex.slice(3, 5), 16) / 255;
    var b = parseInt(hex.slice(5, 7), 16) / 255;
    // 2) petite correction (gamma) demandée par la norme
    function lin(c) { return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }
    // 3) la luminance perçue : l'œil est surtout sensible au vert, peu au bleu
    var L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
    // 4) fond clair -> texte noir ; fond foncé -> texte blanc
    return L > 0.179 ? '#111111' : '#ffffff';
  }

  /* ---------------------------------------------------------------------------
     4. LA COPIE — copier un code hex, avec repli pour vieux navigateurs.
     --------------------------------------------------------------------------- */
  function copy(hex) {
    var done = function (ok) { statusEl.textContent = ok ? (UI[lang].copied + hex) : UI[lang].copyfail; };
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(hex).then(function () { done(true); }, function () { done(false); });
    } else {
      try {
        var ta = document.createElement('textarea');
        ta.value = hex; ta.style.position = 'fixed'; ta.style.opacity = '0';
        document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta);
        done(true);
      } catch (e) { done(false); }
    }
  }

  /* ---------------------------------------------------------------------------
     5. L'AFFICHAGE — construire les 5 pastilles à partir du tableau "colors".
     --------------------------------------------------------------------------- */
  function renderPalette() {
    paletteEl.innerHTML = '';   // on vide la zone avant de la reconstruire
    colors.forEach(function (hex) {
      // Un VRAI <button> (et non une <div>) : il est focusable et utilisable au clavier.
      var b = document.createElement('button');
      b.type = 'button';
      b.className = 'swatch';
      b.style.background = hex;
      b.style.color = readableTextColor(hex);   // texte noir ou blanc, selon le fond
      b.textContent = hex;
      b.setAttribute('aria-label', UI[lang].copyLabel + hex);   // libellé clair pour lecteur d'écran
      b.addEventListener('click', function () { copy(hex); });
      paletteEl.appendChild(b);
    });
  }

  /* ---------------------------------------------------------------------------
     6. LES ACTIONS — nouvelle palette, changement de langue.
     --------------------------------------------------------------------------- */
  function newPalette() {
    colors = [];
    for (var i = 0; i < 5; i++) colors.push(randomHex());
    statusEl.textContent = '';
    renderPalette();
  }

  function setLang(next) {
    lang = next;
    try { localStorage.setItem('palette-lang', lang); } catch (e) {}
    document.documentElement.lang = lang;
    var t = UI[lang];
    document.querySelectorAll('[data-i18n]').forEach(function (el) {
      var k = el.getAttribute('data-i18n'); if (!t[k]) return;
      if (k === 'credit') el.innerHTML = t[k]; else el.textContent = t[k];
    });
    document.querySelectorAll('[data-lang-btn]').forEach(function (bb) {
      bb.setAttribute('aria-pressed', bb.getAttribute('data-lang-btn') === lang ? 'true' : 'false');
    });
    statusEl.textContent = '';
    renderPalette();   // on ré-affiche pour mettre à jour les aria-label dans la bonne langue
  }

  /* ---------------------------------------------------------------------------
     7. LE DÉMARRAGE.
     --------------------------------------------------------------------------- */
  genBtn.addEventListener('click', newPalette);
  document.querySelectorAll('[data-lang-btn]').forEach(function (bb) {
    bb.addEventListener('click', function () { setLang(bb.getAttribute('data-lang-btn')); });
  });

  newPalette();   // une première palette dès l'ouverture
  setLang(lang);  // puis on applique la langue
})();
</script>
</body>
</html>

À toi de jouer

Le projet est solide ; maintenant amuse-toi à le pousser. Trois pistes, de la plus simple à la plus stylée :

  • Ajoute un bouton « verrouiller » sur une couleur, pour la garder quand on régénère.
  • Génère des couleurs harmonieuses (même teinte, luminosités différentes) plutôt que totalement au hasard.
  • Un bouton « copier toute la palette » d'un coup.

À chaque ajout, repose-toi les questions du TD : est-ce cliquable au clavier ? le texte reste-t-il lisible ? l'action est-elle annoncée ?

Projet suivant
Un suivi d'habitudes →

The project: a palette you can grab in one click

Second project, one notch up. Project 1 displayed text; here, we'll actually build elements and interact with them. On the menu: a color palette generator. Five colors side by side, a button to draw new ones, and a click on a color copies its hex code to the clipboard. Exactly the little tool you really open when starting a mockup and hunting for a mood.

Why this project now? Because it introduces something new: we generate HTML on the fly (the five swatches), make it clickable, and read data on it. And that's precisely the ground where the AI will take two shortcuts that "work" on screen… but leave out everyone without a mouse and good eyesight. We reuse the same loop as project 1: code fast, review slowly.

Same constraint as always: a single HTML file, zero dependencies. You'll be able to download it and open it as-is at the end.

Prompt 1: set the frame

As before, we start by framing the request instead of tossing out a vague idea. We specify the look (five colors side by side), the interaction (click = copy), and what we want to see (the hex code on each color):

Create a standalone web mini-app, a single HTML file (HTML + CSS + JS, zero dependencies, no framework). Goal: show 5 random colors side by side, with a "New palette" button. Clicking a color copies its hex code. Show the hex code on each color. Bilingual FR/EN. Simple, readable code.

The AI ships a working version. To make each color clickable, it takes the shortest path: a <div> with an onclick, and white text on top.

function render() {
  palette.innerHTML = '';
  colors.forEach(function (hex) {
    const d = document.createElement('div');
    d.className = 'swatch';
    d.style.background = hex;
    d.style.color = 'white';          // ⚠️ always white text
    d.textContent = hex;
    d.onclick = function () { copy(hex); };  // ⚠️ clickable div
    palette.appendChild(d);
  });
}

On screen, it looks perfect: colors show up, clicking copies. Except "on screen" and "with a mouse" aren't everyone. Two problems are sleeping in those few lines — and the second is sneaky, because it only shows up one time out of ten.

My human review: 3 things the AI let slip

On project 1, the key reflex was security (injection) and contrast. Here, it's mostly accessibility that will speak up — how someone who doesn't use a mouse, or who can't see the screen, can still use the tool. Here are the three points I redid, and why.

1. A clickable <div> is not a button

Putting an onclick on a <div> works… with a mouse, and only with a mouse. For everyone else, that div is invisible: you can't reach it with Tab, it announces no role to a screen reader, and pressing Enter or Space does nothing. A <button> brings all of that for free: keyboard focus, the "button" role, keyboard activation. The rule is simple and holds forever: anything that's clicked and triggers an action is a <button>, not a div in disguise.

2. White text disappears on light colors

This is the nasty one. A hardcoded color: white is great on navy blue… and utterly unreadable on pale yellow or beige. But the colors are random: impossible to guess ahead of time. And like project 1, nothing warns you — you have to hit the wrong draw to see it. The right answer isn't trial and error, it's to compute: we measure each color's luminance (the official WCAG formula, the one for web accessibility) and put the text in black on light colors, white on dark ones. Whatever color comes up, the hex code stays readable.

3. Copying must be announced

When you copy a code, a little "Copied: #xxxxxx" appears. Visible to whoever looks at the screen, silent to whoever uses a screen reader. So we put it in an aria-live="polite" region, which is re-read whenever its content changes. And while we're at it, each color button gets a meaningful aria-label ("Copy color #xxxxxx") instead of letting the screen reader mumble six hex characters out of context.

Prompt 2: harden after review

Three findings, three fixes. We hand it back to the AI with a precise instruction for each:

Three fixes: (1) replace the clickable divs with real <button> (keyboard focus + Enter/Space activation). (2) Stop hardcoding white text: compute the relative luminance (WCAG) of each color and pick black or white text to guarantee contrast. (3) Add aria-live="polite" on the status and an explicit aria-label on each color button.
// Relative luminance (WCAG): black on light colors, white on dark ones
function readableTextColor(hex) {
  var r = parseInt(hex.slice(1,3),16)/255,
      g = parseInt(hex.slice(3,5),16)/255,
      b = parseInt(hex.slice(5,7),16)/255;
  function lin(c){ return c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4); }
  var L = 0.2126*lin(r) + 0.7152*lin(g) + 0.0722*lin(b);
  return L > 0.179 ? '#111111' : '#ffffff';
}

// A real button: focusable and keyboard-activatable
var b = document.createElement('button');
b.type = 'button';
b.style.color = readableTextColor(hex);
b.setAttribute('aria-label', 'Copy color ' + hex);

Notice the through-line with project 1? Contrast, again. Where we chose safe gradients, here we compute it. Two ways to handle the same reflex: never let chance decide readability.

Host and test

Like project 1, it's a static file: drop it on the server, in the same place as the site, and it's online. For testing, keep the good reflexes, plus one specific to this project:

  • Navigate with the keyboard only (Tab key): you must be able to reach each color and copy it with Enter. That's the test that proves the <button> earned their place.
  • Click "New palette" about twenty times: on every color, the hex code must stay readable, light or dark.
  • Check copying (paste elsewhere), the FR / EN toggle, and the console (zero error).

The finished result

The finished project, live, right in the page:

Open the project full screen

The full code (and downloadable)

The entire file, exactly the one running above. Download it, open it, read it.

Download the code (.html · 243 lines)

View the full code
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Générateur de palette de couleurs</title>
<meta name="description" content="Génère une palette de couleurs et copie un code hex en un clic. Mini-projet construit avec l'IA — web-developpeur.com">
<meta name="robots" content="noindex, follow">
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">

<!--
  ============================================================================
  GÉNÉRATEUR DE PALETTE — un seul fichier : HTML + CSS + JS, zéro dépendance.
    1. <style>  : l'apparence (les 5 pastilles, les boutons, le responsive).
    2. <body>   : la structure (en-tête + zone palette + bouton).
    3. <script> : la logique (tirer des couleurs, choisir noir/blanc, copier).
  Point clé du projet : le texte de chaque couleur passe en noir OU blanc
  automatiquement, selon la luminosité du fond, pour rester toujours lisible.
  ============================================================================
-->

<style>
  :root { --ink:#1a1d24; --muted:#5a6270; --accent:#267d42; --border:#e2e6ea; }
  * { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; }
  body {
    font-family: 'Segoe UI', system-ui, -apple-system, Roboto, Helvetica, Arial, sans-serif;
    background: #f4f6f8; color: var(--ink); min-height: 100vh;
    display: flex; flex-direction: column; align-items: center;
    padding: 32px 18px 48px; line-height: 1.5;
  }
  .app { width: 100%; max-width: 680px; }
  header { text-align: center; margin-bottom: 22px; }
  h1 { font-size: 1.5rem; margin: 0 0 6px; letter-spacing: -0.01em; }
  .sub { color: var(--muted); font-size: 0.95rem; margin: 0 0 16px; }

  .lang-switch { display: inline-flex; border: 1.5px solid var(--border); border-radius: 999px; overflow: hidden; background: #fff; }
  .lang-btn { border: 0; background: transparent; padding: 7px 18px; font: inherit; font-weight: 700; font-size: 0.8rem; color: var(--muted); cursor: pointer; }
  .lang-btn[aria-pressed="true"] { background: var(--accent); color: #fff; }
  .lang-btn:focus-visible { outline: 3px solid rgba(38,125,66,0.4); outline-offset: 2px; }

  /* La palette = 5 pastilles côte à côte (flex). Chaque pastille est un <button>. */
  .palette { display: flex; gap: 0; border-radius: 16px; overflow: hidden; box-shadow: 0 16px 38px rgba(15,25,35,0.16); }
  .swatch {
    flex: 1 1 0; min-height: 220px; border: 0; cursor: pointer;
    display: flex; align-items: flex-end; justify-content: center;
    padding-bottom: 18px; font-family: 'JetBrains Mono', ui-monospace, monospace;
    font-weight: 700; font-size: 0.95rem; letter-spacing: 0.02em;
    transition: flex-grow 0.2s ease;
  }
  .swatch:hover { flex-grow: 1.25; }                              /* la pastille survolée s'élargit un peu */
  .swatch:focus-visible { outline: 3px solid #111; outline-offset: -3px; }   /* contour clavier */

  .actions { display: flex; gap: 12px; margin-top: 22px; flex-wrap: wrap; justify-content: center; }
  .btn { min-height: 50px; padding: 0 26px; border-radius: 12px; border: 1.5px solid transparent; font: inherit; font-weight: 700; font-size: 0.98rem; cursor: pointer; transition: transform 0.12s ease, box-shadow 0.2s ease, background 0.2s ease; }
  .btn-primary { background: var(--accent); color: #fff; box-shadow: 0 6px 16px rgba(38,125,66,0.28); }
  .btn-primary:hover { background: #1f6a37; transform: translateY(-1px); }
  .btn:focus-visible { outline: 3px solid rgba(38,125,66,0.4); outline-offset: 2px; }

  .hint { text-align: center; color: var(--muted); font-size: 0.85rem; margin-top: 14px; }
  .status { text-align: center; min-height: 20px; margin-top: 6px; font-size: 0.85rem; font-weight: 600; color: var(--accent); }
  .credit { text-align: center; color: var(--muted); font-size: 0.78rem; margin-top: 28px; }
  .credit a { color: var(--accent); }

  /* Sur petit écran : les pastilles s'empilent verticalement. */
  @media (max-width: 560px) {
    .palette { flex-direction: column; }
    .swatch { min-height: 60px; align-items: center; padding-bottom: 0; }
    .swatch:hover { flex-grow: 1; }
  }
  @media (prefers-reduced-motion: reduce) { .swatch, .btn { transition: none; } }
</style>
</head>
<body>

<main class="app">
  <header>
    <h1 data-i18n="title">Palette de couleurs</h1>
    <p class="sub" data-i18n="sub">Génère une palette, clique une couleur pour copier son code hex.</p>
    <div class="lang-switch" role="group" aria-label="Langue / Language">
      <button class="lang-btn" data-lang-btn="fr" aria-pressed="true" type="button">FR</button>
      <button class="lang-btn" data-lang-btn="en" aria-pressed="false" type="button">EN</button>
    </div>
  </header>

  <!-- Cette zone est vide au départ : le JS y insère les 5 pastilles. -->
  <div class="palette" id="palette" role="group" aria-label="Palette"></div>

  <div class="actions">
    <button class="btn btn-primary" id="gen" type="button" data-i18n="gen">Nouvelle palette</button>
  </div>

  <p class="hint" data-i18n="hint">Clique une couleur pour copier son code hex.</p>
  <p class="status" id="status" role="status" aria-live="polite"></p>

  <p class="credit" data-i18n="credit">Mini-projet du cours <a href="/apprendre/projets/">Projets appliqués</a> — web-developpeur.com</p>
</main>

<script>
(function () {
  'use strict';

  /* ---------------------------------------------------------------------------
     1. LES TEXTES DE L'INTERFACE (traduits FR / EN).
     --------------------------------------------------------------------------- */
  var UI = {
    fr: {
      title: "Palette de couleurs",
      sub: "Génère une palette, clique une couleur pour copier son code hex.",
      gen: "Nouvelle palette",
      hint: "Clique une couleur pour copier son code hex.",
      credit: 'Mini-projet du cours <a href="/apprendre/projets/">Projets appliqués</a> — web-developpeur.com',
      copied: "Copié : ",
      copyfail: "Copie impossible — sélectionne le code à la main.",
      copyLabel: "Copier la couleur "
    },
    en: {
      title: "Color palette",
      sub: "Generate a palette, click a color to copy its hex code.",
      gen: "New palette",
      hint: "Click a color to copy its hex code.",
      credit: 'A mini-project from the <a href="/apprendre/projets/">Applied projects</a> course — web-developpeur.com',
      copied: "Copied: ",
      copyfail: "Couldn't copy — select the code manually.",
      copyLabel: "Copy color "
    }
  };

  /* ---------------------------------------------------------------------------
     2. LES ÉLÉMENTS DE LA PAGE + L'ÉTAT (la langue, les couleurs affichées).
     --------------------------------------------------------------------------- */
  var paletteEl = document.getElementById('palette');
  var statusEl = document.getElementById('status');
  var genBtn = document.getElementById('gen');

  var lang = 'fr';
  try { lang = localStorage.getItem('palette-lang') || 'fr'; } catch (e) {}
  if (lang !== 'fr' && lang !== 'en') lang = 'fr';

  var colors = [];   // les 5 codes hex de la palette en cours

  /* ---------------------------------------------------------------------------
     3. LES OUTILS COULEUR — tirer une couleur, choisir un texte lisible.
     --------------------------------------------------------------------------- */

  // Tire une couleur au hasard et la renvoie au format "#rrggbb".
  function randomHex() {
    var n = Math.floor(Math.random() * 0x1000000);     // un nombre entre 0 et 16 777 215
    return '#' + ('000000' + n.toString(16)).slice(-6); // converti en hexa, complété à 6 caractères
  }

  // Renvoie '#111111' (noir) ou '#ffffff' (blanc) selon la LUMINOSITÉ du fond,
  // pour que le texte reste lisible quelle que soit la couleur. (Formule officielle WCAG.)
  function readableTextColor(hex) {
    // 1) on sépare les composantes rouge / vert / bleu, ramenées entre 0 et 1
    var r = parseInt(hex.slice(1, 3), 16) / 255;
    var g = parseInt(hex.slice(3, 5), 16) / 255;
    var b = parseInt(hex.slice(5, 7), 16) / 255;
    // 2) petite correction (gamma) demandée par la norme
    function lin(c) { return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }
    // 3) la luminance perçue : l'œil est surtout sensible au vert, peu au bleu
    var L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
    // 4) fond clair -> texte noir ; fond foncé -> texte blanc
    return L > 0.179 ? '#111111' : '#ffffff';
  }

  /* ---------------------------------------------------------------------------
     4. LA COPIE — copier un code hex, avec repli pour vieux navigateurs.
     --------------------------------------------------------------------------- */
  function copy(hex) {
    var done = function (ok) { statusEl.textContent = ok ? (UI[lang].copied + hex) : UI[lang].copyfail; };
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(hex).then(function () { done(true); }, function () { done(false); });
    } else {
      try {
        var ta = document.createElement('textarea');
        ta.value = hex; ta.style.position = 'fixed'; ta.style.opacity = '0';
        document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta);
        done(true);
      } catch (e) { done(false); }
    }
  }

  /* ---------------------------------------------------------------------------
     5. L'AFFICHAGE — construire les 5 pastilles à partir du tableau "colors".
     --------------------------------------------------------------------------- */
  function renderPalette() {
    paletteEl.innerHTML = '';   // on vide la zone avant de la reconstruire
    colors.forEach(function (hex) {
      // Un VRAI <button> (et non une <div>) : il est focusable et utilisable au clavier.
      var b = document.createElement('button');
      b.type = 'button';
      b.className = 'swatch';
      b.style.background = hex;
      b.style.color = readableTextColor(hex);   // texte noir ou blanc, selon le fond
      b.textContent = hex;
      b.setAttribute('aria-label', UI[lang].copyLabel + hex);   // libellé clair pour lecteur d'écran
      b.addEventListener('click', function () { copy(hex); });
      paletteEl.appendChild(b);
    });
  }

  /* ---------------------------------------------------------------------------
     6. LES ACTIONS — nouvelle palette, changement de langue.
     --------------------------------------------------------------------------- */
  function newPalette() {
    colors = [];
    for (var i = 0; i < 5; i++) colors.push(randomHex());
    statusEl.textContent = '';
    renderPalette();
  }

  function setLang(next) {
    lang = next;
    try { localStorage.setItem('palette-lang', lang); } catch (e) {}
    document.documentElement.lang = lang;
    var t = UI[lang];
    document.querySelectorAll('[data-i18n]').forEach(function (el) {
      var k = el.getAttribute('data-i18n'); if (!t[k]) return;
      if (k === 'credit') el.innerHTML = t[k]; else el.textContent = t[k];
    });
    document.querySelectorAll('[data-lang-btn]').forEach(function (bb) {
      bb.setAttribute('aria-pressed', bb.getAttribute('data-lang-btn') === lang ? 'true' : 'false');
    });
    statusEl.textContent = '';
    renderPalette();   // on ré-affiche pour mettre à jour les aria-label dans la bonne langue
  }

  /* ---------------------------------------------------------------------------
     7. LE DÉMARRAGE.
     --------------------------------------------------------------------------- */
  genBtn.addEventListener('click', newPalette);
  document.querySelectorAll('[data-lang-btn]').forEach(function (bb) {
    bb.addEventListener('click', function () { setLang(bb.getAttribute('data-lang-btn')); });
  });

  newPalette();   // une première palette dès l'ouverture
  setLang(lang);  // puis on applique la langue
})();
</script>
</body>
</html>

Your turn

The project is solid; now have fun pushing it. Three ideas, from simplest to slickest:

  • Add a "lock" button on a color, to keep it when regenerating.
  • Generate harmonious colors (same hue, different lightness) instead of fully random.
  • A "copy the whole palette" button at once.

On every addition, ask the build's questions again: is it keyboard-clickable? does the text stay readable? is the action announced?

Next project
A habit tracker →
Accepter ou rejeter le code de l'IA

Tu lui as demandé de remplacer la <div> cliquable par un vrai bouton. L'IA renvoie ça. Tu l'acceptes tel quel ou tu le rejettes ?

const d = document.createElement('div');
d.className = 'swatch';
d.setAttribute('role', 'button');
d.style.color = 'white';
d.onclick = function () { copy(hex); };
Rejeter : c'est un faux correctif. Coller role="button" sur une <div> ment au lecteur d'écran (il annonce « bouton ») mais ne donne rien d'autre : pas de focus clavier, pas d'activation à Entrée/Espace. Et le color: white en dur est toujours là, illisible sur les couleurs claires. La vraie réponse reste celle du TD : un <button> natif (qui apporte focus + clavier gratuitement) et une couleur de texte calculée par luminance.
Rappel libre

Sans remonter dans la leçon : pourquoi une couleur cliquable doit-elle être un <button> et pas une <div>, et pourquoi calcule-t-on la luminance au lieu d'écrire color: white en dur ?

Le <button> apporte gratuitement le focus clavier, le rôle « bouton » annoncé au lecteur d'écran et l'activation à Entrée/Espace ; une <div> avec onclick ne marche qu'à la souris. Côté texte, les couleurs sont tirées au hasard : color: white en dur devient illisible sur un fond clair. On mesure donc la luminance relative (formule WCAG) et on choisit noir ou blanc, pour que le code hex reste lisible quelle que soit la couleur.
Prochaine étape

Ton générateur de palette crache maintenant des couleurs harmonieuses à volonté. Au projet suivant, tu construis un suivi d'habitudes : une appli qui mémorise tes coches jour après jour et te montre tes séries.

Leçon 3 : Suivi d'habitudes →
Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement