Skip to content

Instantly share code, notes, and snippets.

@statico
Created March 12, 2026 00:45
Show Gist options
  • Select an option

  • Save statico/b04b5cd82a85258184100afa08cce665 to your computer and use it in GitHub Desktop.

Select an option

Save statico/b04b5cd82a85258184100afa08cce665 to your computer and use it in GitHub Desktop.
Math Tutor - single page HTML app for 9-year-old level math practice
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Math Tutor</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #f0f4ff;
color: #1a1a2e;
min-height: 100vh;
padding: 20px;
}
h1 {
text-align: center;
font-size: 2rem;
margin-bottom: 20px;
color: #2d3a8c;
}
.container {
max-width: 700px;
margin: 0 auto;
}
/* Settings panel */
.settings {
background: #fff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
margin-bottom: 20px;
}
.settings h2 {
font-size: 1.1rem;
margin-bottom: 14px;
color: #444;
}
.ops {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.ops button {
font-size: 1.2rem;
padding: 10px 22px;
border: 2px solid #c5cae9;
border-radius: 10px;
background: #e8eaf6;
cursor: pointer;
transition: all 0.15s;
font-weight: 600;
}
.ops button.active {
background: #3f51b5;
color: #fff;
border-color: #3f51b5;
}
.ops button:hover:not(.active) {
background: #d1d5f0;
}
.size-control {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.size-control label {
font-weight: 600;
min-width: 110px;
}
.size-control input[type=range] {
flex: 1;
min-width: 120px;
accent-color: #3f51b5;
}
.size-control .size-val {
font-weight: 700;
min-width: 36px;
text-align: center;
font-size: 1.1rem;
color: #3f51b5;
}
.size-hint {
font-size: 0.82rem;
color: #888;
margin-top: 4px;
}
/* Problem area */
.problem-area {
background: #fff;
border-radius: 16px;
padding: 28px 24px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
text-align: center;
margin-bottom: 20px;
}
.problem {
font-size: 2.4rem;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 20px;
font-family: 'Courier New', monospace;
}
.problem .op-sym {
color: #3f51b5;
margin: 0 10px;
}
.answer-row {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
#answer {
font-size: 1.8rem;
padding: 8px 16px;
border: 2px solid #c5cae9;
border-radius: 10px;
width: 180px;
text-align: center;
font-family: 'Courier New', monospace;
outline: none;
transition: border 0.2s;
}
#answer:focus { border-color: #3f51b5; }
.btn {
font-size: 1.1rem;
padding: 10px 24px;
border: none;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
transition: background 0.15s;
}
.btn-check {
background: #43a047;
color: #fff;
}
.btn-check:hover { background: #388e3c; }
.btn-new {
background: #3f51b5;
color: #fff;
}
.btn-new:hover { background: #303f9f; }
.btn-show {
background: #ff9800;
color: #fff;
}
.btn-show:hover { background: #ef6c00; }
.buttons {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
/* Result feedback */
.feedback {
font-size: 1.4rem;
font-weight: 700;
margin: 14px 0 6px;
min-height: 36px;
}
.feedback.correct { color: #2e7d32; }
.feedback.wrong { color: #c62828; }
/* Explanation area */
.explanation {
background: #fffde7;
border: 2px solid #ffe082;
border-radius: 14px;
padding: 22px 20px;
margin-top: 20px;
text-align: left;
font-family: 'Courier New', monospace;
font-size: 1.05rem;
line-height: 1.7;
white-space: pre;
overflow-x: auto;
display: none;
}
.explanation h3 {
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 1.1rem;
color: #e65100;
margin-bottom: 10px;
white-space: normal;
}
.explanation .step {
white-space: pre;
margin-bottom: 2px;
}
.explanation .note {
font-family: 'Segoe UI', system-ui, sans-serif;
color: #5d4037;
font-size: 0.95rem;
white-space: normal;
margin: 6px 0;
padding-left: 4px;
font-style: italic;
}
/* Score */
.score-bar {
text-align: center;
font-size: 0.95rem;
color: #666;
margin-bottom: 10px;
}
.score-bar span { font-weight: 700; color: #3f51b5; }
/* Responsive */
@media (max-width: 500px) {
.problem { font-size: 1.7rem; }
#answer { font-size: 1.4rem; width: 140px; }
.explanation { font-size: 0.92rem; }
}
</style>
</head>
<body>
<div class="container">
<h1>Math Tutor</h1>
<div class="score-bar">Score: <span id="score">0</span> / <span id="total">0</span></div>
<div class="settings">
<h2>Operation</h2>
<div class="ops">
<button class="active" data-op="add">+ Add</button>
<button data-op="sub">&minus; Subtract</button>
<button data-op="mul">&times; Multiply</button>
<button data-op="div">&divide; Divide</button>
</div>
<h2>Number Size</h2>
<div class="size-control">
<label for="sizeRange">Difficulty:</label>
<input type="range" id="sizeRange" min="1" max="4" step="0.5" value="2">
<div class="size-val" id="sizeVal">2</div>
</div>
<div class="size-hint" id="sizeHint">Two 2-digit numbers</div>
</div>
<div class="problem-area">
<div class="problem" id="problem"></div>
<div class="answer-row">
<input type="text" id="answer" placeholder="?" autocomplete="off" inputmode="numeric">
<button class="btn btn-check" id="checkBtn" onclick="checkAnswer()">Check</button>
</div>
<div class="buttons">
<button class="btn btn-show" onclick="showExplanation()">Show How</button>
<button class="btn btn-new" onclick="newProblem()">New Problem</button>
</div>
<div class="feedback" id="feedback"></div>
<div class="explanation" id="explanation"></div>
</div>
</div>
<script>
let state = { a: 0, b: 0, op: 'add', answer: 0, checked: false };
let score = 0, total = 0;
// --- Operation buttons ---
document.querySelectorAll('.ops button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.ops button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.op = btn.dataset.op;
newProblem();
});
});
// --- Size slider ---
const sizeRange = document.getElementById('sizeRange');
const sizeVal = document.getElementById('sizeVal');
const sizeHint = document.getElementById('sizeHint');
sizeRange.addEventListener('input', () => {
sizeVal.textContent = sizeRange.value;
updateSizeHint();
newProblem();
});
function updateSizeHint() {
const v = parseFloat(sizeRange.value);
const d1 = Math.ceil(v), d2 = Math.floor(v);
if (d1 === d2) {
sizeHint.textContent = `Two ${d1}-digit number${d1>1?'s':''}`;
} else {
sizeHint.textContent = `One ${d1}-digit number and one ${d2}-digit number`;
}
}
function randDigits(n) {
if (n <= 0) return 1;
const lo = n === 1 ? 1 : Math.pow(10, n - 1);
const hi = Math.pow(10, n) - 1;
return Math.floor(Math.random() * (hi - lo + 1)) + lo;
}
function newProblem() {
const v = parseFloat(sizeRange.value);
const d1 = Math.ceil(v), d2 = Math.floor(v);
// Randomly assign bigger/smaller digit count
let a, b;
if (Math.random() < 0.5) { a = randDigits(d1); b = randDigits(d2); }
else { a = randDigits(d2); b = randDigits(d1); }
// For subtraction, ensure a >= b (no negatives for a 9-year-old)
if (state.op === 'sub' && a < b) [a, b] = [b, a];
// For division, generate a problem that divides evenly
if (state.op === 'div') {
// b is divisor, result is quotient with digits based on size
b = randDigits(d2 || 1);
if (b === 0) b = 1;
const quotient = randDigits(d1);
a = b * quotient;
}
state.a = a;
state.b = b;
state.checked = false;
switch (state.op) {
case 'add': state.answer = a + b; break;
case 'sub': state.answer = a - b; break;
case 'mul': state.answer = a * b; break;
case 'div': state.answer = a / b; break;
}
const opSymbols = { add: '+', sub: '−', mul: '×', div: '÷' };
document.getElementById('problem').innerHTML =
`${a} <span class="op-sym">${opSymbols[state.op]}</span> ${b}`;
document.getElementById('answer').value = '';
document.getElementById('feedback').textContent = '';
document.getElementById('feedback').className = 'feedback';
document.getElementById('explanation').style.display = 'none';
document.getElementById('answer').focus();
}
function checkAnswer() {
const input = document.getElementById('answer').value.trim();
if (input === '') return;
const userAns = parseFloat(input);
const fb = document.getElementById('feedback');
if (!state.checked) { total++; }
if (Math.abs(userAns - state.answer) < 0.0001) {
if (!state.checked) score++;
fb.textContent = 'Correct!';
fb.className = 'feedback correct';
} else {
fb.textContent = `Not quite — try again or click "Show How"`;
fb.className = 'feedback wrong';
}
state.checked = true;
document.getElementById('score').textContent = score;
document.getElementById('total').textContent = total;
}
// Enter key submits
document.getElementById('answer').addEventListener('keydown', e => {
if (e.key === 'Enter') checkAnswer();
});
// --- Explanation generators ---
function showExplanation() {
const el = document.getElementById('explanation');
let html = '';
switch (state.op) {
case 'add': html = explainAdd(state.a, state.b); break;
case 'sub': html = explainSub(state.a, state.b); break;
case 'mul': html = explainMul(state.a, state.b); break;
case 'div': html = explainDiv(state.a, state.b); break;
}
el.innerHTML = html;
el.style.display = 'block';
// Also reveal correct answer
const fb = document.getElementById('feedback');
fb.textContent = `Answer: ${state.answer}`;
fb.className = 'feedback correct';
if (!state.checked) { total++; state.checked = true; }
document.getElementById('total').textContent = total;
}
function pad(s, n) { return s.toString().padStart(n, ' '); }
function explainAdd(a, b) {
const sum = a + b;
const w = Math.max(a.toString().length, b.toString().length, sum.toString().length) + 2;
// Column-by-column carry explanation
const sa = a.toString(), sb = b.toString();
const maxLen = Math.max(sa.length, sb.length);
const pa = sa.padStart(maxLen, '0');
const pb = sb.padStart(maxLen, '0');
let steps = [];
let carry = 0;
for (let i = maxLen - 1; i >= 0; i--) {
const da = parseInt(pa[i]), db = parseInt(pb[i]);
const col = da + db + carry;
const newCarry = Math.floor(col / 10);
const digit = col % 10;
const pos = maxLen - i;
let note = `Column ${pos} (from right): ${da} + ${db}`;
if (carry) note += ` + ${carry} (carry)`;
note += ` = ${col}`;
if (newCarry) note += ` → write ${digit}, carry ${newCarry}`;
steps.push(note);
carry = newCarry;
}
if (carry) steps.push(`Carry the ${carry} to the front`);
let out = `<h3>Column Addition</h3>`;
out += `<div class="step">${pad(a, w)}</div>`;
out += `<div class="step">+${pad(b, w - 1)}</div>`;
out += `<div class="step">${'─'.repeat(w)}</div>`;
out += `<div class="step">${pad(sum, w)}</div>\n`;
steps.forEach(s => { out += `<div class="note">${s}</div>`; });
return out;
}
function explainSub(a, b) {
const diff = a - b;
const w = Math.max(a.toString().length, b.toString().length, diff.toString().length) + 2;
const sa = a.toString(), sb = b.toString();
const maxLen = Math.max(sa.length, sb.length);
const pa = sa.padStart(maxLen, '0');
const pb = sb.padStart(maxLen, '0');
let steps = [];
let borrow = 0;
for (let i = maxLen - 1; i >= 0; i--) {
let da = parseInt(pa[i]), db = parseInt(pb[i]) + borrow;
const pos = maxLen - i;
let note = `Column ${pos}: ${parseInt(pa[i])}`;
if (borrow) note += ` (after borrowing: ${da} )`;
note += ` − ${parseInt(pb[i])}`;
if (borrow) note += ` (+ ${borrow} borrow = ${db})`;
if (da < db) {
note += ` → ${da} is less than ${db}, so borrow 10 → ${da+10} − ${db} = ${da + 10 - db}`;
borrow = 1;
} else {
note += ` = ${da - db}`;
borrow = 0;
}
steps.push(note);
}
let out = `<h3>Column Subtraction</h3>`;
out += `<div class="step">${pad(a, w)}</div>`;
out += `<div class="step">−${pad(b, w - 1)}</div>`;
out += `<div class="step">${'─'.repeat(w)}</div>`;
out += `<div class="step">${pad(diff, w)}</div>\n`;
steps.forEach(s => { out += `<div class="note">${s}</div>`; });
return out;
}
function explainMul(a, b) {
const product = a * b;
// Use the smaller number as the multiplier for partial products
let top = a, bottom = b;
if (b.toString().length > a.toString().length) { top = b; bottom = a; }
const sb = bottom.toString();
const w = Math.max(top.toString().length + sb.length + 2, product.toString().length + 2);
let out = `<h3>Long Multiplication</h3>`;
out += `<div class="step">${pad(top, w)}</div>`;
out += `<div class="step">×${pad(bottom, w - 1)}</div>`;
out += `<div class="step">${'─'.repeat(w)}</div>`;
let partials = [];
for (let i = sb.length - 1; i >= 0; i--) {
const d = parseInt(sb[i]);
const placeVal = Math.pow(10, sb.length - 1 - i);
const partial = top * d;
const shifted = partial * placeVal;
partials.push({ digit: d, partial, shifted, placeVal, placeIdx: sb.length - 1 - i });
const label = placeVal > 1 ? ` (${d} × ${top} = ${partial}, shifted ${sb.length-1-i} place${sb.length-1-i>1?'s':''})` : ` (${d} × ${top})`;
out += `<div class="step">${pad(shifted, w)}</div>`;
out += `<div class="note">${label}</div>`;
}
if (partials.length > 1) {
out += `<div class="step">${'─'.repeat(w)}</div>`;
out += `<div class="step">${pad(product, w)}</div>`;
out += `<div class="note">Add the partial products: ${partials.map(p => p.shifted).join(' + ')} = ${product}</div>`;
}
return out;
}
function explainDiv(a, b) {
const quotient = Math.floor(a / b);
const sq = quotient.toString();
const sa = a.toString();
let out = `<h3>Long Division</h3>`;
// Draw the bracket
const totalW = sa.length + 4;
out += `<div class="step"> ${sq}</div>`;
out += `<div class="step"> ${'─'.repeat(sa.length + 1)}</div>`;
out += `<div class="step"> ${b} │ ${a}</div>`;
// Step through long division
let remainder = 0;
let steps = [];
for (let i = 0; i < sa.length; i++) {
const bring = parseInt(sa[i]);
const current = remainder * 10 + bring;
const d = Math.floor(current / b);
const sub = d * b;
const newRem = current - sub;
let note = '';
if (i === 0) {
note = `Bring down ${bring} → ${current}. How many times does ${b} go into ${current}? ${d} time${d!==1?'s':''}. ${d} × ${b} = ${sub}. Remainder: ${current} − ${sub} = ${newRem}`;
} else {
note = `Bring down ${bring} → ${current}. ${b} goes into ${current} ${d} time${d!==1?'s':''}. ${d} × ${b} = ${sub}. Remainder: ${current} − ${sub} = ${newRem}`;
}
steps.push(note);
remainder = newRem;
}
out += `\n`;
steps.forEach((s, i) => {
out += `<div class="note">Step ${i + 1}: ${s}</div>`;
});
out += `\n<div class="note">Result: ${a} ÷ ${b} = ${quotient}</div>`;
return out;
}
// Init
updateSizeHint();
newProblem();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment