---
format: 1
concept: "binary-search"
kind: "figure"
title: "Watch the range shrink"
labels: {"lang":"en","level":"beginner","approach":"visual"}
base: "a3042e7ca8d007dd70cf82f2ee672a0a71852fb03deb14a853846e9cac223865"
changes: ["Uses the ai.page figure kit: the shared design tokens for colour, light and dark, and the shared controls"]
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":[],"sources":[]}
---
Fifteen numbered boxes in a row, sorted from smallest to largest, and a target number to find. Each press of **Next step** looks at the middle box of the range still in play, says whether it's smaller or bigger than the target, and greys out the half that can't hold it. The step count and a sentence describing each step are shown in text below the boxes. **New target** picks another number (sometimes one that isn't in the list), and the arrow keys also step and reset. With reduced motion, boxes change without animation.

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{display:flex;flex-direction:column;gap:14px;min-height:100vh;box-sizing:border-box}
.row{display:grid;grid-template-columns:repeat(15,1fr);gap:4px}
.box{border:1px solid var(--aip-line);border-radius:6px;text-align:center;padding:8px 0;font-variant-numeric:tabular-nums;font-size:clamp(11px,2.6vw,16px);transition:background .25s ease-out,color .25s ease-out,opacity .25s ease-out}
.box.out{opacity:.35;background:var(--aip-figure-shade-2)}
.box.mid{background:var(--aip-accent);color:var(--aip-paper);border-color:var(--aip-accent);font-weight:700}
.box.found{outline:3px solid var(--aip-accent);outline-offset:2px}
.idx{display:block;font-size:10px;color:var(--aip-ink-muted)}
.box.mid .idx{color:inherit}
.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
#say{min-height:3em}
.target{font-size:20px;font-weight:700}
@media (prefers-reduced-motion:reduce){.box{transition:none}}
</style>
</head>
<body>
<div class="bar"><span>Find <span class="target" id="target"></span></span><span id="count" style="color:var(--aip-ink-muted)"></span></div>
<div class="row" id="row" role="list" aria-label="Sorted numbers"></div>
<p id="say" aria-live="polite"></p>
<div class="bar"><button class="go" id="step">Next step</button><button id="again">New target</button></div>
<script>
const values = [2,5,8,12,16,23,38,45,56,61,72,77,85,91,99];
const row = document.getElementById("row"), say = document.getElementById("say"), stepBtn = document.getElementById("step");
const boxes = values.map((v, i) => { const d = document.createElement("div"); d.className = "box"; d.setAttribute("role", "listitem"); d.innerHTML = `${v}<span class="idx">${i}</span>`; row.append(d); return d; });
let target, lo, hi, steps, done;
function draw(mid) {
  boxes.forEach((b, i) => { b.classList.toggle("out", i < lo || i > hi); b.classList.toggle("mid", i === mid); });
  document.getElementById("count").textContent = `· ${steps} step${steps === 1 ? "" : "s"}`;
}
function reset() {
  const pool = [...values, 30, 70, 3, 100];
  let next; do { next = pool[Math.floor(Math.random() * pool.length)]; } while (next === target);
  target = next; lo = 0; hi = values.length - 1; steps = 0; done = false;
  boxes.forEach((b) => b.classList.remove("found"));
  document.getElementById("target").textContent = target;
  say.textContent = `The whole list, positions 0 to ${hi}, could hold ${target}. Press Next step.`;
  stepBtn.disabled = false; draw(-1);
}
function step() {
  if (done) return;
  if (lo > hi) { say.textContent = `Nothing is left to search: ${target} isn't in the list. That took ${steps} steps.`; done = true; stepBtn.disabled = true; draw(-1); return; }
  const mid = Math.floor((lo + hi) / 2), v = values[mid]; steps++;
  if (v === target) { say.textContent = `Position ${mid} holds ${v}. Found in ${steps} step${steps === 1 ? "" : "s"}.`; boxes[mid].classList.add("found"); done = true; stepBtn.disabled = true; draw(mid); return; }
  if (v < target) { say.textContent = `Position ${mid} holds ${v}, which is smaller than ${target}, so everything up to it goes. Search positions ${mid + 1} to ${hi}.`; draw(mid); lo = mid + 1; }
  else { say.textContent = `Position ${mid} holds ${v}, which is bigger than ${target}, so everything from it on goes. Search positions ${lo} to ${mid - 1}.`; draw(mid); hi = mid - 1; }
  setTimeout(() => draw(-1), matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 700);
}
stepBtn.addEventListener("click", step);
document.getElementById("again").addEventListener("click", reset);
addEventListener("keydown", (e) => { if (e.key === "ArrowRight") step(); if (e.key === "ArrowLeft") reset(); });
reset();
</script>
</body>
</html>
```

```json
{
  "libraries": [],
  "strings": {
    "step": "Next step",
    "again": "New target"
  }
}
```
