Building a Middle School Quiz App in React: Gamification, Accessibility and Adaptive Questions

My nephew has his Brevet exam in June — France's national middle school exam. He revises with paper flashcards, forgets everything in two days, and would rather watch YouTube shorts than re-read his math notes. Classic situation. I wanted to build him something motivating — not yet another revision PDF, but something that feels more like a game than homework. The result: an interactive quiz platform covering math, physics-chemistry and biology, from 6th to 9th grade.

React on the front, zero backend (localStorage), and way more work on gamification and accessibility than on the code itself.

The problem with existing quiz tools

There are dozens of Brevet revision tools online. But most share the same flaws: identical questions every time, no progress visualization, and a UX that makes you want to close the tab. A 14-year-old isn't going to stick around on a page that looks like a government form. They need instant feedback, rewards, and the feeling of making progress.

The other problem is that most quizzes treat all questions equally. A trigonometry exercise (coefficient 3 in the Brevet) weighs the same as a basic arithmetic question. A student can score 15/20 on the quiz and still bomb the exam because their gaps were concentrated in the highest-weighted domains.

30 randomized questions, weighted scoring

Each quiz picks 2 to 4 questions per domain from a bank of ~50 questions, for a total of about 30 questions per session. The student never gets the same sequence twice.

Scoring works on two axes: a raw score (all questions equal weight) and a weighted score out of 20 that reflects the actual Brevet coefficients. Equations and linear functions count 4 times more than basic arithmetic. This completely changes how students perceive their own performance — someone who nails mental calculation but struggles with trigonometry sees it immediately in the gap between their two scores.

// Domain coefficients (reflect actual Brevet weight)
const DOMAIN_WEIGHTS = {
    arithmetic:    2,
    powers:        2,
    identities:    3,
    equations:     4,
    functions:     4,
    pythagoras:    3,
    thales:        3,
    trigonometry:  3,
    geometry3d:    2,
    statistics:    2
};

// Weighted score = sum(correct × coeff) / sum(coeff) × 20
function computeWeightedScore(results) {
    let totalWeight = 0, earnedWeight = 0;
    for (const [domain, answers] of Object.entries(results)) {
        const w = DOMAIN_WEIGHTS[domain];
        totalWeight += answers.length * w;
        earnedWeight += answers.filter(a => a.correct).length * w;
    }
    return (earnedWeight / totalWeight) * 20;
}

Skills radar and progress curve

The end-of-quiz report is where I spent the most time. A simple "you got 14/20" isn't enough — the student needs to understand where they're strong and where they need to work.

The skills radar (a dynamically generated SVG) overlays the current quiz profile with the average profile from previous attempts. At a glance, you can see whether trigonometry is improving or still the weak spot. Each domain has its own detailed card: success rate, coefficient, and a toggle to review specific mistakes with hints.

The SVG progress curve plots the weighted score across recent attempts. It's simple, but seeing an upward curve has a massive psychological effect on a teenager's motivation. Far more effective than a table of scores.

// SVG radar generation — one polygon per data series
function drawRadar(ctx, domains, current, average) {
    const cx = 150, cy = 150, r = 120;
    const n = domains.length;
    const angleStep = (2 * Math.PI) / n;

    function toPoints(scores) {
        return scores.map((s, i) => {
            const angle = angleStep * i - Math.PI / 2;
            const ratio = s / 100;
            return `${cx + r * ratio * Math.cos(angle)},${cy + r * ratio * Math.sin(angle)}`;
        }).join(' ');
    }

    return `
        <svg viewBox="0 0 300 300">
            <polygon points="${toPoints(average)}" class="radar-average"/>
            <polygon points="${toPoints(current)}" class="radar-current"/>
            ${domains.map((d, i) => {
                const angle = angleStep * i - Math.PI / 2;
                return `<text x="${cx + (r+20) * Math.cos(angle)}"
                              y="${cy + (r+20) * Math.sin(angle)}">${d}</text>`;
            }).join('')}
        </svg>`;
}

Adaptive questions: targeting weak spots

This is the mechanism that makes the real difference versus a basic random quiz. Every wrong answer gets logged in a weakness tracker (localStorage). On the next quiz, frequently missed questions have 1 to 3 times higher probability of being selected.

In practice, if the student consistently misses Thales theorem questions, they'll see more of them next time — not so many that the quiz becomes repetitive, but enough for the brain to anchor the methods. It's simplified spaced repetition, without the complexity of a full Anki-style algorithm.

// Weighted selection: frequently missed questions come back more
function selectQuestions(pool, wrongTracker, count) {
    const weighted = pool.map(q => ({
        ...q,
        weight: 1 + (wrongTracker[q.id] || 0)  // base 1, +1 per past error
    }));

    const selected = [];
    while (selected.length < count && weighted.length > 0) {
        const totalWeight = weighted.reduce((sum, q) => sum + q.weight, 0);
        let random = Math.random() * totalWeight;
        const idx = weighted.findIndex(q => (random -= q.weight) <= 0);
        selected.push(weighted.splice(idx, 1)[0]);
    }
    return selected;
}

Gamification: badges, streaks and encouragement

A badge system rewards behaviors, not just results. The first completed quiz unlocks a badge. Five consecutive days unlocks another. There's a badge for trying all three subjects, one for covering all grade levels from 6th to 9th, and one for improving by more than 3 points between two attempts.

The daily streak is calculated on calendar dates — no need to do 10 quizzes per day, one is enough to keep the series going. The idea is to build a regular habit rather than marathon sessions the night before the exam. The countdown to exam day (displayed for 9th graders) adds urgency without creating stress.

An encouragement message pops up halfway through ("You're halfway there, keep going!") and a confetti animation triggers on badge unlocks. Cosmetic details, but that's exactly what makes a teenager come back the next day.

Accessibility: dyslexia mode and keyboard navigation

A toggle activates the OpenDyslexic font, increases line height and adjusts letter spacing. This isn't a gimmick — roughly 5 to 10% of students are dyslexic, and standard school assessments almost never offer this option. Accommodations exist for the actual Brevet exam (extra time, etc.), but revision tools don't account for it.

Keyboard navigation is complete: arrow keys to move between questions, keys 1 to 4 to select an answer, Enter to validate. The app also respects prefers-reduced-motion to disable animations for users who've configured it. Interactive elements are hidden from print via no-print for those who want to print their report.

Zero accounts, zero backend

Everything is stored in localStorage. No account creation, no password to remember, no server to maintain. The student types their first name and they're off. Results persist across sessions as long as the browser cache isn't cleared.

This is a deliberate choice. For a tool used by a middle schooler on the family PC, requiring email registration would be an immediate friction point. The trade-off is that data doesn't survive a browser switch or cache clear — but for a temporary revision tool (a few months before the Brevet), that's perfectly acceptable.

The only server call is an optional save.php to persist results server-side as a backup. But the quiz works perfectly without it — all computation, scoring and chart rendering happens in the browser.

Conclusion

The most interesting part of this project wasn't the React code itself — it's fairly standard state management. The real work was in the pedagogical design: which coefficients to assign, how to visualize progress without discouraging, when to encourage, how to calibrate adaptive difficulty so it stays motivating without becoming frustrating.

A revision tool that actually works is 20% code and 80% psychology. My nephew has been using it every day for two weeks. The curve is going up. We'll see in June.

Comments (0)