Classroom bingo where every student gets their own card

Bingo is one of the easiest ways to make thirty students listen to a word list. The hard part used to be making thirty different cards.

Thirty students, one word list, and you want them all listening.

Bingo does that. Every word you read out is a word they hunt for on their card.

The work used to be the cards. Thirty cards, all different, printed, cut, handed out, and collected again so nobody keeps the same one.

These days you can skip that. Ask ChatGPT, Claude or Gemini for a bingo page that shuffles itself, and every student who opens the link gets their own card.

Here is a student card. Tap words to mark them. Get five in a row, a column or a diagonal.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vocabulary bingo</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; background: radial-gradient(circle at 20% 0%, #ffe3f1, transparent 45%), radial-gradient(circle at 100% 30%, #dff1ff, transparent 40%), #fffaf2; color: #2a2140; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
  .page { max-width: 520px; margin: 0 auto; padding: 20px 14px 26px; }
  .top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
  h1 { margin: 0; font-size: 22px; }
  h1 span { color: #e0457b; }
  .top button { border: 0; border-radius: 12px; padding: 9px 12px; background: #2a2140; color: #fff; font: 600 13px system-ui; cursor: pointer; }
  .head { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; margin-bottom: 6px; }
  .head span { text-align: center; font: 900 24px system-ui; color: #fff; border-radius: 10px; padding: 4px 0; }
  .grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
  .cell { aspect-ratio: 1; display: grid; place-items: center; padding: 4px; border: 0; border-radius: 12px; background: #fff; box-shadow: 0 3px 0 #e6dcf3; font: 700 clamp(11px, 3.3vw, 15px)/1.15 system-ui; color: #2a2140; cursor: pointer; text-align: center; word-break: break-word; transition: transform .12s; }
  .cell:active { transform: scale(.95); }
  .cell.long { font-size: clamp(9px, 2.6vw, 12px); }
  .cell.on { background: #7b5cf0; color: #fff; box-shadow: 0 3px 0 #5a3fd0; }
  .cell.free { background: #ffd23f; color: #2a2140; box-shadow: 0 3px 0 #e0b21f; cursor: default; }
  .cell.win { background: #2bb673; color: #fff; box-shadow: 0 3px 0 #1d8a55; animation: pop .5s; }
  @keyframes pop { 50% { transform: scale(1.12); } }
  .banner { display: none; margin-top: 12px; padding: 14px; border-radius: 16px; background: #2bb673; color: #fff; text-align: center; font: 900 26px system-ui; letter-spacing: .04em; }
  .words { margin-top: 18px; padding: 12px 14px; border-radius: 14px; background: rgba(255,255,255,.7); font-size: 13px; color: #5c5275; }
  .words h2 { margin: 0 0 6px; font-size: 12px; letter-spacing: .1em; text-transform: uppercase; }
  .words ul { margin: 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 4px 10px; }
  canvas { position: fixed; inset: 0; pointer-events: none; }
</style>
</head>
<body>
<main class="page">
  <div class="top"><h1>Weather <span>bingo</span></h1><button id="new">New card</button></div>
  <div class="head"><span style="background:#e0457b">B</span><span style="background:#f2994a">I</span><span style="background:#2bb673">N</span><span style="background:#3a8dde">G</span><span style="background:#7b5cf0">O</span></div>
  <div class="grid" id="grid"></div>
  <div class="banner" id="banner">BINGO!</div>

  <section class="words">
    <h2>This week's words</h2>
    <ul id="words">
      <li>drizzle</li><li>thunder</li><li>lightning</li><li>forecast</li><li>humid</li><li>breeze</li>
      <li>blizzard</li><li>drought</li><li>flood</li><li>hail</li><li>fog</li><li>frost</li>
      <li>rainbow</li><li>hurricane</li><li>tornado</li><li>sleet</li><li>cloudy</li><li>sunny</li>
      <li>temperature</li><li>season</li><li>climate</li><li>puddle</li><li>gust</li><li>mist</li>
      <li>storm</li><li>shower</li><li>degree</li><li>snowflake</li><li>monsoon</li><li>heatwave</li>
    </ul>
  </section>
</main>
<canvas id="fx"></canvas>
<script>
  const grid = document.getElementById('grid'), banner = document.getElementById('banner');
  const LINES = [];
  for (let i = 0; i < 5; i++) { LINES.push([0, 1, 2, 3, 4].map((k) => i * 5 + k)); LINES.push([0, 1, 2, 3, 4].map((k) => k * 5 + i)); }
  LINES.push([0, 6, 12, 18, 24], [4, 8, 12, 16, 20]);

  function deal() {
    // Words come from the list on the page, so editing that list changes every new card.
    const words = [...document.querySelectorAll('#words li')].map((li) => li.textContent.trim()).filter(Boolean);
    const pick = words.sort(() => Math.random() - 0.5).slice(0, 24);
    grid.innerHTML = ''; banner.style.display = 'none';
    for (let i = 0; i < 25; i++) {
      const b = document.createElement('button'); b.className = 'cell';
      if (i === 12) { b.textContent = 'FREE'; b.classList.add('free', 'on'); }
      else { b.textContent = pick[i < 12 ? i : i - 1] || '?'; if (b.textContent.length > 8) b.classList.add('long'); b.onclick = () => { b.classList.toggle('on'); check(); }; }
      grid.append(b);
    }
  }

  function check() {
    const cells = [...grid.children];
    cells.forEach((c) => c.classList.remove('win'));
    const won = LINES.filter((l) => l.every((i) => cells[i].classList.contains('on')));
    won.flat().forEach((i) => cells[i].classList.add('win'));
    const was = banner.style.display === 'block';
    banner.style.display = won.length ? 'block' : 'none';
    if (won.length && !was) confetti();
  }

  function confetti() {
    const cv = document.getElementById('fx'), x = cv.getContext('2d');
    cv.width = innerWidth; cv.height = innerHeight;
    const cols = ['#e0457b', '#f2994a', '#2bb673', '#3a8dde', '#7b5cf0', '#ffd23f'];
    const bits = Array.from({ length: 140 }, () => ({ x: Math.random() * cv.width, y: -20 - Math.random() * cv.height * .4, vx: (Math.random() - .5) * 3, vy: 2 + Math.random() * 3, r: Math.random() * 6, c: cols[Math.floor(Math.random() * 6)], a: Math.random() * 6 }));
    let f = 0;
    (function draw() {
      x.clearRect(0, 0, cv.width, cv.height);
      bits.forEach((b) => { b.x += b.vx; b.y += b.vy; b.a += .1; x.fillStyle = b.c; x.save(); x.translate(b.x, b.y); x.rotate(b.a); x.fillRect(-4, -2, 8, 4 + b.r); x.restore(); });
      if (++f < 150) requestAnimationFrame(draw); else x.clearRect(0, 0, cv.width, cv.height);
    })();
  }

  document.getElementById('new').onclick = deal;
  deal();
</script>
</body>
</html>
A vocabulary bingo card that shuffles from a word list every time it opens. Tap to mark, and the page celebrates when you complete a line. The word list sits at the bottom of the page. Edit the code and the example reruns.

Three things that make it a good game

1. More words than squares. A 5 by 5 card has 24 words and a free space. With exactly 24 words every card has the same words, just moved around. With 30 to 40, cards really differ and nobody wins on the same call.

2. Read out a clue, not the word. "A word that means very happy" makes students think. "Delighted" just makes them scan. The card shows the word; you read the meaning.

3. Check it yourself. Ask for the page to highlight the winning line. When a student calls bingo, you look at one glowing row instead of reading 25 squares.

Two screens. Students see their own shuffled card; the teacher's screen draws the next word and keeps a list of words already called.
Two screens. Students see their own shuffled card; the teacher's screen draws the next word and keeps a list of words already called.

Make it with AI

Open ChatGPT, Claude or Gemini and paste this. Change the words in capitals and paste your list at the end.

Make one complete HTML file: classroom bingo.
Topic: TOPIC, for GRADE / AGE.
Card: 5 by 5, free space in the middle. Fill the
other 24 squares from my word list in a random
order every time the page opens. Tap to mark a
square. When a row, column or diagonal is done,
highlight it and show "Bingo!". Add a "New card"
button.
Show my word list as a plain list at the bottom of
the page, and read the words from that list.
Rules: large text, works on a tablet, all CSS and
JavaScript inside the file, no images from other
sites, nothing sent anywhere.
Word list: PASTE 30 TO 40 WORDS HERE

"Read the words from that list" matters. It keeps your words as plain text on the page, which makes changing them later a matter of editing a list.

For the front of the room, ask for a caller screen with the same list. Press the button, a word comes up large, and the ones already called stay visible.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bingo caller</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; min-height: 100vh; background: #2a2140; color: #fff; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
  .page { max-width: 760px; margin: 0 auto; padding: 22px 18px; }
  .bar { display: flex; justify-content: space-between; align-items: center; color: #c8bfe6; font-size: 14px; }
  .stage { margin: 18px 0; padding: 30px 16px; border-radius: 24px; background: linear-gradient(135deg, #7b5cf0, #e0457b); text-align: center; box-shadow: 0 16px 40px rgba(0,0,0,.3); }
  .stage small { display: block; font-size: 13px; letter-spacing: .14em; text-transform: uppercase; opacity: .8; }
  .word { margin-top: 6px; font: 900 clamp(38px, 10vw, 64px)/1.05 system-ui; }
  .word.flip { animation: flip .45s; }
  @keyframes flip { from { transform: rotateX(90deg); opacity: 0; } to { transform: none; opacity: 1; } }
  .btns { display: flex; gap: 10px; justify-content: center; }
  .btns button { border: 0; border-radius: 14px; padding: 14px 20px; font: 800 16px system-ui; cursor: pointer; }
  #draw { background: #ffd23f; color: #2a2140; min-width: 180px; }
  #reset { background: rgba(255,255,255,.12); color: #fff; }
  h2 { margin: 22px 0 8px; font-size: 13px; letter-spacing: .1em; text-transform: uppercase; color: #c8bfe6; }
  .called { display: flex; flex-wrap: wrap; gap: 6px; }
  .called span { padding: 6px 10px; border-radius: 999px; background: rgba(255,255,255,.12); font-size: 14px; }
  .called span:first-child { background: #ffd23f; color: #2a2140; font-weight: 700; }
  #words { margin: 0; padding: 12px 14px; list-style: none; display: flex; flex-wrap: wrap; gap: 4px 10px; border-radius: 14px; background: rgba(255,255,255,.06); font-size: 12.5px; color: #c8bfe6; }
</style>
</head>
<body>
<main class="page">
  <div class="bar"><span>Weather bingo - caller</span><span id="left"></span></div>
  <div class="stage"><small>Next word</small><div class="word" id="word">Ready?</div></div>
  <div class="btns"><button id="draw">Draw a word</button><button id="reset">New game</button></div>
  <h2>Called so far</h2>
  <div class="called" id="called"></div>
  <h2>Word list</h2>
  <ul id="words">
    <li>drizzle</li><li>thunder</li><li>lightning</li><li>forecast</li><li>humid</li><li>breeze</li>
    <li>blizzard</li><li>drought</li><li>flood</li><li>hail</li><li>fog</li><li>frost</li>
    <li>rainbow</li><li>hurricane</li><li>tornado</li><li>sleet</li><li>cloudy</li><li>sunny</li>
    <li>temperature</li><li>season</li><li>climate</li><li>puddle</li><li>gust</li><li>mist</li>
    <li>storm</li><li>shower</li><li>degree</li><li>snowflake</li><li>monsoon</li><li>heatwave</li>
  </ul>
</main>
<script>
  let bag = [], called = [];
  const $ = (id) => document.getElementById(id);
  function reset() {
    bag = [...document.querySelectorAll('#words li')].map((li) => li.textContent.trim()).filter(Boolean).sort(() => Math.random() - 0.5);
    called = []; $('word').textContent = 'Ready?'; render();
  }
  function render() {
    $('called').innerHTML = called.map((w) => '<span>' + w + '</span>').join('');
    $('left').textContent = bag.length + ' left';
    $('draw').disabled = !bag.length;
  }
  $('draw').onclick = () => {
    if (!bag.length) return;
    const w = bag.pop(); called.unshift(w);
    const el = $('word'); el.classList.remove('flip'); void el.offsetWidth; el.textContent = w; el.classList.add('flip');
    render();
  };
  $('reset').onclick = reset;
  reset();
</script>
</body>
</html>
A bingo caller screen for the teacher: draw the next word with one click, see every word called so far, and start a new game. Edit the code and the example reruns.

Getting the cards to the class

You have a bingo game. The AI gave it to you as HTML code, though, and code does not shuffle anything for a student holding a tablet.

The first wall is getting the page onto thirty screens. Attach the file and some students download it, some cannot open it on a school tablet, and some see code. Print it, and you are back to one card for everyone.

In NOS you paste the code into a document and it becomes the game straight away. Choose Create share link and put that link in your class post, on the board, or in a QR code. Each student who opens it gets a different card. How to send an HTML file in Google Classroom covers the Classroom side.

One link opened on three student tablets, each with a different card.
One link opened on three student tablets, each with a different card.

The second wall comes with next week's words. The unit moves on. Two of the words were too easy. You want "precipitation" instead of "sunny".

If you ask the AI again, you wait for a new reply. It often changes the layout or the rules while it is at it, so you test the game again. Then you have a new file and a new link, and some students open last week's.

In NOS you click the word in the list at the bottom of the page and type the new one. The link stays the same, and the next card anyone opens uses the new word.

"sunny" changed to "precipitation" in the word list on the page in NOS. The same link now deals cards with the new word.
"sunny" changed to "precipitation" in the word list on the page in NOS. The same link now deals cards with the new word.

What it does not do

Each card lives in one student's browser. The page does not know who has which card or who won, and it does not send anything back.

That is how paper bingo works too. Students call it, you check the highlighted line on their screen.

For a game with points and teams, see Jeopardy template for the classroom. For practice with instant feedback, see Interactive worksheet.

Questions people ask

How do I make a different bingo card for each student?

Let the page shuffle. When a student opens the link, the page draws their card from your word list in a random order, so no two cards are the same. A "New card" button gives another one.

Can AI make a classroom bingo game?

Yes. Ask for one complete HTML file and give it your word list. Ask for the card to shuffle on every open and to show a message when a row, column or diagonal is complete.

Does the page know who won?

No. Each card lives in that student’s browser and nothing is sent back. Students call "Bingo!" and you check their screen, the same as with paper.

Do students need accounts?

No. A NOS share link opens in an ordinary browser on a tablet, laptop or phone.

Can I change the words next week?

Yes. Edit the word list on the page in NOS. Students who open the same link get cards with the new words.

Keep reading