Last month I watched a couple fight on the subway. She kept saying "you never understand me." He kept saying "you're overreacting." It gave me an idea.
I wanted to build honest relationship quizzes — the kind that make you actually pause. No login walls, no upsells, no "premium to see your result."
Here's how I built all 8 in vanilla JS with zero dependencies.
The Architecture
Each quiz is a single self-contained HTML file. No build step, no npm, no framework. Just HTML + CSS + JS in one file.
Why? Because it loads instantly, works offline, is easy to host anywhere, and anyone can inspect the code.
Two Scoring Models
Type-based (Attachment Style, Love Language, Conflict Style):
Each option maps to a type. At the end, the dominant type wins.
const Qs = [
{ q: "Your partner says 'I need space.' Your reaction?", opts: [
{ t: "Of course — take the time you need.", type: 'S' },
{ t: "Did I do something wrong?", type: 'X' },
{ t: "Great, I could use space too.", type: 'V' },
{ t: "Part of me wants to chase, part wants to disappear.", type: 'F' }
]}
];
function getDominant() {
return ORDER.reduce((a, b) => counts[b] > counts[a] ? b : a, 'S');
}
Linear scoring (Red Flag, Green Flag, Emotional Availability):
Options score 1–4. Normalize with (total - min) / (max - min) * 100.
const minP = Qs.reduce((s, q) => s + Math.min(...q.opts.map(o => o.s)), 0);
const maxP = Qs.reduce((s, q) => s + Math.max(...q.opts.map(o => o.s)), 0);
const pct = Math.round((total - minP) / (maxP - minP) * 100);
Options Shuffle (Without Breaking Scores)
The key insight: shuffle the option objects, not just the text. The score/type stays bound to the object.
const shuffle = a => [...a].sort(() => Math.random() - 0.5);
// In renderQ():
const shuffled = shuffle(q.opts);
// onclick="pick('${o.type}', this)" — type travels with the option
localStorage Progress Save
Users can close mid-quiz and return:
// Save on each answer
localStorage.setItem('ast_state', JSON.stringify({ cur: cur + 1, typeCounts }));
// Restore on start
const sv = localStorage.getItem('ast_state');
if (sv) {
const st = JSON.parse(sv);
if (st.typeCounts && st.cur > 0 && st.cur < Qs.length) {
cur = st.cur;
typeCounts = st.typeCounts;
}
}
// Clear on completion
localStorage.removeItem('ast_state');
Canvas Share Card
Each result generates a downloadable 1080x1080 PNG:
function downloadCard() {
const cv = document.createElement('canvas');
cv.width = 1080; cv.height = 1080;
const ctx = cv.getContext('2d');
// gradient background, score ring/bars, result text...
const a = document.createElement('a');
a.download = 'result.png';
a.href = cv.toDataURL('image/png');
a.click();
}
The Bug I Spent Too Long On
Unescaped apostrophes inside single-quoted JS strings. 'You don't lose yourself' breaks silently — the page loads but the entire script fails to parse, so clicking the start button does nothing.
Fix: change to double quotes for any string containing apostrophes.
// BROKEN
strengths: ['You don\'t lose yourself in relationships']
// FIXED
strengths: ["You don't lose yourself in relationships"]
Live Demo
https://ordinarymantrying.com/tools/relationship-lab/
All 8 tests are free, no login required. The Attachment Style and Conflict Style tests are probably the most useful to take together.
What would you do differently? Always curious how others approach quiz scoring without a backend.
Top comments (0)