DEV Community

Văn Tuấn Lê
Văn Tuấn Lê

Posted on

How I Built 8 Free Relationship Quizzes With Vanilla JS (No Framework, No Backend)

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');
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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)