Immersive full-screen multi-step testing wizard with particle tunnel backgrounds
A data-driven, API-ready 3D testing experience with timed multi-step questions, pass/fail scoring, detailed answer review, confetti completion effects, and smooth GSAP + anime.js transitions. Fully responsive mobile-first layout with native scroll.
| Feature | |
|---|---|
| 🧪 | Test-Focused Flow — Built for quizzes/exams with score calculation and pass/fail logic |
| ⏱️ | Countdown Timer — Optional per-test time limit with automatic submit on timeout |
| 📊 | Live Progress — Animated progress bar + step indicators across all questions |
| 🧠 | Data-Driven — One JSON-like config controls the entire test |
| 🧩 | Multiple Question Types — select, multiselect, yesno, number, text, textarea, rating, slider, emoji, email, date |
| ✅ | Validation — Required-field validation before moving to the next step |
| 🧾 | Answer Review — Result screen shows user answers vs correct answers with explanations |
| 🚀 | 3D Visuals — Particle tunnel background with mood transitions and celebration mode |
| 🎉 | Completion Effects — Canvas-confetti burst + animated result entry |
| 📱 | Responsive UX — Mobile-first design with native scroll and touch-safe controls |
| Layer | Technology |
|---|---|
| Framework | Preact 10 |
| 3D Engine | Three.js |
| Transitions | GSAP 3 |
| Micro-motion | anime.js |
| Celebration | canvas-confetti |
| Styling | Tailwind CSS 3 |
| Build | Vite 5 |
- Node.js 18+ and npm 9+
npm install
npm run devnpm run build
npm run preview- Push repository to GitHub
- Connect repo in Netlify
- Build settings are preconfigured in
netlify.toml - Deploy
Everything lives in src/data/testData.js.
{
id: "frontend-essentials-test",
title: "Frontend Essentials Test",
subtitle: "8 quick questions...",
version: "1.0.0",
branding: {
logoText: "✦ TestLab 3D",
completionEmoji: "🏁"
},
settings: {
showProgressBar: true,
showStepIndicators: true,
showStepCount: true,
submitButtonText: "Finish Test",
nextButtonText: "Next",
prevButtonText: "Back",
passThreshold: 70,
timeLimitSec: 360
},
steps: [ /* ... */ ]
}{
id: "q1",
title: "JavaScript Fundamentals",
subtitle: "Choose one correct answer",
icon: "🧠",
fields: [ /* ... */ ]
}{
id: "js_scope",
type: "select",
label: "Which keyword creates a block-scoped variable?",
required: true,
options: [{ value: "let", label: "let" }],
// scoring fields
correctAnswer: "let",
explanation: "Shown in result review"
}- Only fields that have
correctAnswerare included in scoring. multiselectanswers are compared as unordered sets.- String answers are compared case-insensitively (trimmed).
- Other types are compared by strict equality.
- Pass/fail is determined by
settings.passThreshold(default fallback:70).
Return the same schema as testConfig.
// App.jsx sketch
const TEST_CONFIG_URL = import.meta.env.VITE_TEST_CONFIG_URL;
const [config, setConfig] = useState(testConfig); // local fallback
useEffect(() => {
if (!TEST_CONFIG_URL) return;
fetch(TEST_CONFIG_URL)
.then((res) => {
if (!res.ok) throw new Error(`Config load failed: ${res.status}`);
return res.json();
})
.then((remoteConfig) => setConfig(remoteConfig))
.catch((err) => {
console.error("Config load error:", err);
// fallback stays as local testConfig
});
}, []);TestWizard already emits this object to onComplete(finalResult):
{
answers: { [fieldId]: value },
timedOut: boolean,
elapsedSec: number,
secondsLeft: number | null
}// App.jsx sketch
const RESULT_URL = import.meta.env.VITE_TEST_RESULT_URL;
const handleTestComplete = async (finalResult) => {
setResult(finalResult);
setAppState("completed");
if (!RESULT_URL) return;
await fetch(RESULT_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
testId: config.id,
completedAt: new Date().toISOString(),
...finalResult
})
});
};{
"testId": "frontend-essentials-test",
"completedAt": "2026-03-14T00:00:00.000Z",
"timedOut": false,
"elapsedSec": 142,
"secondsLeft": 218,
"answers": {
"js_scope": "let",
"state_mutation": false,
"promise_states": ["pending", "fulfilled", "rejected"]
}
}VITE_TEST_CONFIG_URL— endpoint for loading test configVITE_TEST_RESULT_URL— endpoint for posting final test result
preact-threejs-3d-testing-lab/
├── index.html
├── netlify.toml
├── package.json
├── vite.config.js
├── tailwind.config.js
├── postcss.config.js
├── src/
│ ├── main.jsx
│ ├── App.jsx
│ ├── styles/
│ │ └── index.css
│ ├── data/
│ │ └── testData.js
│ └── components/
│ ├── ThreeBackground.jsx
│ ├── TestWizard.jsx
│ ├── ProgressBar.jsx
│ ├── StepIndicators.jsx
│ ├── QuestionRenderer.jsx
│ └── ResultsScreen.jsx
└── README.md
The Three.js canvas is position: fixed behind a naturally scrollable content layer.
- ✅ No content clipping on small screens
- ✅ Native mobile scroll behavior
- ✅ Input-safe font sizing (
16px) to prevent iOS zoom - ✅ Smooth touch + keyboard interaction
MIT — use freely in personal and commercial projects.
Testing at light speed • Forged with Preact, Three.js & endless ☕