Clinic — Symptom Checker Wizard
A friendly multi-step symptom checker: pick a body area, select symptoms, set duration and severity, then see an illustrative triage result with a colored level banner, recommendation, next steps, and a clear non-diagnostic disclaimer.
MCP
الكود
:root {
--teal: #129c93;
--teal-d: #0c7a73;
--teal-700: #0a655f;
--teal-50: #e7f5f3;
--coral: #ff7a66;
--coral-soft: #ffe6df;
--ink: #16322f;
--ink-2: #3a534f;
--muted: #6b827e;
--bg: #f1f7f6;
--white: #ffffff;
--line: rgba(16, 50, 47, 0.1);
--line-2: rgba(16, 50, 47, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--pending: #6b7280;
--danger: #d4503e;
--font: "Inter", system-ui, -apple-system, sans-serif;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(16, 50, 47, 0.05), 0 4px 14px rgba(16, 50, 47, 0.06);
--shadow-2: 0 16px 40px rgba(12, 122, 115, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
/* ── App bar ── */
.appbar {
background: var(--white);
border-bottom: 1px solid var(--line);
padding: 14px 28px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--ink);
}
.brand-mark {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 10px;
background: linear-gradient(150deg, var(--teal), var(--teal-d));
color: #fff;
font-size: 1.1rem;
font-weight: 700;
}
.brand-name {
font-size: 1.1rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.brand-name em {
font-style: normal;
color: var(--teal-d);
}
.appbar-tag {
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--teal-d);
background: var(--teal-50);
border-radius: 999px;
padding: 6px 12px;
}
/* ── Layout ── */
.wizard {
max-width: 680px;
margin: 0 auto;
padding: 28px 24px 56px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* ── Progress ── */
.progress {
display: flex;
flex-direction: column;
gap: 10px;
}
.progress-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.progress-label {
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--teal-d);
}
.progress-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--ink-2);
}
.progress-track {
height: 8px;
border-radius: 999px;
background: var(--white);
border: 1px solid var(--line);
overflow: hidden;
}
.progress-fill {
display: block;
height: 100%;
width: 25%;
border-radius: 999px;
background: linear-gradient(90deg, var(--teal), var(--teal-d));
transition: width 0.35s ease;
}
/* ── Card ── */
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 26px 26px 20px;
box-shadow: var(--shadow-1);
}
/* ── Steps ── */
.step {
border: none;
display: none;
}
.step.is-active {
display: block;
animation: rise 0.3s ease;
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.step-h {
font-size: 1.3rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.step-sub {
color: var(--muted);
font-size: 0.92rem;
margin: 4px 0 18px;
}
/* ── Step 1 — Body area ── */
.area-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.area-card {
background: var(--white);
border: 1.5px solid var(--line);
border-radius: var(--r-md);
padding: 18px 12px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font: inherit;
font-weight: 600;
font-size: 0.88rem;
color: var(--ink);
cursor: pointer;
transition: transform 0.14s, box-shadow 0.14s, border-color 0.14s, background 0.14s;
}
.area-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-1);
border-color: var(--teal);
}
.area-card[aria-checked="true"] {
border-color: var(--teal);
background: var(--teal-50);
color: var(--teal-700);
}
.area-ic {
font-size: 1.6rem;
line-height: 1;
}
/* ── Step 2 — Symptom chips ── */
.chip-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1.5px solid var(--line);
border-radius: 999px;
padding: 10px 16px;
font-size: 0.9rem;
font-weight: 500;
color: var(--ink-2);
cursor: pointer;
transition: border-color 0.14s, background 0.14s, color 0.14s;
}
.chip:hover {
border-color: var(--teal);
}
.chip input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.chip:has(input:checked) {
border-color: var(--teal);
background: var(--teal-50);
color: var(--teal-700);
font-weight: 600;
}
.chip:has(input:focus-visible) {
outline: 2px solid var(--teal);
outline-offset: 2px;
}
/* ── Step 3 — Duration & severity ── */
.field {
margin-bottom: 22px;
}
.field:last-child {
margin-bottom: 0;
}
.field-label {
font-size: 0.84rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-2);
margin-bottom: 12px;
}
.radio-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.radio {
display: flex;
align-items: center;
gap: 10px;
border: 1.5px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: border-color 0.14s, background 0.14s;
}
.radio:hover {
border-color: var(--teal);
}
.radio input {
accent-color: var(--teal-d);
width: 16px;
height: 16px;
}
.radio:has(input:checked) {
border-color: var(--teal);
background: var(--teal-50);
color: var(--teal-700);
font-weight: 600;
}
.sev-value {
display: inline-grid;
place-items: center;
min-width: 30px;
height: 26px;
padding: 0 8px;
border-radius: 999px;
background: var(--teal-d);
color: #fff;
font-size: 0.9rem;
font-weight: 700;
margin-left: 6px;
vertical-align: middle;
}
.sev-of {
color: var(--muted);
font-weight: 600;
margin-left: 4px;
}
.slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 8px;
border-radius: 999px;
background: var(--teal-50);
outline: none;
margin: 6px 0 8px;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--teal-d);
border: 3px solid var(--white);
box-shadow: var(--shadow-1);
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--teal-d);
border: 3px solid var(--white);
box-shadow: var(--shadow-1);
cursor: pointer;
}
.slider-scale {
display: flex;
justify-content: space-between;
font-size: 0.76rem;
color: var(--muted);
font-weight: 500;
}
/* ── Step 4 — Result ── */
.result {
display: flex;
flex-direction: column;
gap: 16px;
}
.result-banner {
border-radius: var(--r-md);
padding: 20px 22px;
display: flex;
flex-direction: column;
gap: 6px;
border-left: 5px solid var(--pending);
background: rgba(107, 114, 128, 0.1);
}
.result-banner.level-danger {
border-left-color: var(--danger);
background: rgba(212, 80, 62, 0.1);
}
.result-banner.level-warn {
border-left-color: var(--warn);
background: rgba(217, 138, 43, 0.12);
}
.result-banner.level-ok {
border-left-color: var(--ok);
background: rgba(47, 158, 111, 0.12);
}
.rb-level {
font-size: 1.2rem;
font-weight: 800;
letter-spacing: -0.01em;
}
.level-danger .rb-level {
color: var(--danger);
}
.level-warn .rb-level {
color: var(--warn);
}
.level-ok .rb-level {
color: var(--ok);
}
.rb-rec {
font-size: 0.94rem;
color: var(--ink-2);
}
.next-steps {
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
}
.next-steps li {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 0.92rem;
color: var(--ink-2);
}
.next-steps li::before {
content: "✓";
flex: none;
width: 22px;
height: 22px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--teal-50);
color: var(--teal-d);
font-size: 0.78rem;
font-weight: 800;
margin-top: 1px;
}
.disclaimer {
background: var(--coral-soft);
border: 1px solid rgba(255, 122, 102, 0.4);
border-radius: var(--r-md);
padding: 14px 16px;
font-size: 0.86rem;
color: var(--ink-2);
line-height: 1.55;
}
.disclaimer strong {
color: var(--danger);
}
/* ── Buttons ── */
.btn {
border: none;
border-radius: 11px;
padding: 11px 22px;
font: inherit;
font-weight: 600;
font-size: 0.92rem;
cursor: pointer;
transition: transform 0.12s, background 0.15s, opacity 0.15s;
}
.btn:active {
transform: translateY(1px);
}
.btn.primary {
background: var(--coral);
color: #fff;
}
.btn.primary:hover {
background: #ff8e7c;
}
.btn.primary:disabled {
background: var(--line-2);
color: var(--muted);
cursor: default;
}
.btn.ghost {
background: var(--white);
border: 1px solid var(--line-2);
color: var(--ink-2);
}
.btn.ghost:hover {
background: var(--teal-50);
border-color: var(--teal);
}
.btn.block {
width: 100%;
}
/* ── Nav ── */
.nav {
display: flex;
align-items: center;
gap: 12px;
margin-top: 22px;
padding-top: 18px;
border-top: 1px solid var(--line);
}
.nav-hint {
flex: 1;
font-size: 0.82rem;
color: var(--danger);
font-weight: 500;
text-align: center;
min-height: 1em;
}
#nextBtn {
margin-left: auto;
}
@media (max-width: 560px) {
.area-grid {
grid-template-columns: repeat(2, 1fr);
}
.radio-grid {
grid-template-columns: 1fr;
}
.card {
padding: 20px 18px 16px;
}
}// ── State ────────────────────────────────────────────────────────────────────
const TOTAL_STEPS = 4;
const STEP_NAMES = ["Body area", "Symptoms", "Duration & severity", "Result"];
const state = {
step: 1,
area: null,
symptoms: [],
duration: "<24h",
severity: 5,
};
const form = document.getElementById("wizardForm");
const steps = [...form.querySelectorAll(".step")];
const stepLabel = document.getElementById("stepLabel");
const stepName = document.getElementById("stepName");
const progressBar = document.getElementById("progressBar");
const progressFill = document.getElementById("progressFill");
const backBtn = document.getElementById("backBtn");
const nextBtn = document.getElementById("nextBtn");
const restartBtn = document.getElementById("restartBtn");
const navHint = document.getElementById("navHint");
// ── Render the current step ──────────────────────────────────────────────────
function render() {
steps.forEach((s) => s.classList.toggle("is-active", Number(s.dataset.step) === state.step));
stepLabel.textContent = `Step ${state.step} of ${TOTAL_STEPS}`;
stepName.textContent = STEP_NAMES[state.step - 1];
progressFill.style.width = `${(state.step / TOTAL_STEPS) * 100}%`;
progressBar.setAttribute("aria-valuenow", String(state.step));
navHint.textContent = "";
const onResult = state.step === TOTAL_STEPS;
backBtn.hidden = state.step === 1 || onResult;
nextBtn.hidden = onResult;
restartBtn.hidden = !onResult;
nextBtn.textContent = state.step === TOTAL_STEPS - 1 ? "See result" : "Next";
}
// ── Validation gating ────────────────────────────────────────────────────────
function validateStep() {
if (state.step === 1 && !state.area) {
return "Please choose a body area to continue.";
}
if (state.step === 2 && state.symptoms.length === 0) {
return "Please select at least one symptom.";
}
return "";
}
// ── Deterministic illustrative triage ────────────────────────────────────────
function computeTriage() {
const hasUrgentSymptom = state.symptoms.includes("Shortness of breath");
const chestPain = state.area === "Chest" && state.symptoms.includes("Pain");
const longDuration = state.duration === "4-7 days" || state.duration === ">1 week";
if (hasUrgentSymptom || chestPain || state.severity >= 8) {
return {
key: "danger",
level: "Seek urgent care",
rec: "Your answers suggest symptoms that should be assessed promptly by a clinician.",
steps: [
"Contact urgent care or your clinic today.",
"If breathing is difficult or pain is severe, call your local emergency number.",
"Have someone stay with you if you feel unwell.",
],
};
}
if (state.severity >= 5 || longDuration || state.symptoms.length >= 3) {
return {
key: "warn",
level: "Book a GP appointment soon",
rec: "Your symptoms are worth a professional review within the next day or two.",
steps: [
"Book a routine appointment with your GP.",
"Note when symptoms started and what makes them better or worse.",
"Rest, stay hydrated, and monitor for any new symptoms.",
],
};
}
return {
key: "ok",
level: "Self-care & monitor",
rec: "Your symptoms appear mild. Home care and observation are reasonable for now.",
steps: [
"Rest and keep up your fluids.",
"Use over-the-counter relief as appropriate.",
"Check back in if symptoms worsen or last beyond a few days.",
],
};
}
function renderResult() {
const t = computeTriage();
const banner = document.getElementById("resultBanner");
banner.className = `result-banner level-${t.key}`;
document.getElementById("rbLevel").textContent = t.level;
document.getElementById("rbRec").textContent = t.rec;
const list = document.getElementById("nextSteps");
list.innerHTML = "";
for (const step of t.steps) {
const li = document.createElement("li");
li.textContent = step;
list.appendChild(li);
}
}
// ── Step 1 — body area (single select) ───────────────────────────────────────
document.getElementById("areaGrid").addEventListener("click", (e) => {
const card = e.target.closest(".area-card");
if (!card) return;
state.area = card.dataset.area;
for (const c of card.parentElement.children) {
c.setAttribute("aria-checked", String(c === card));
}
navHint.textContent = "";
});
// ── Step 2 — symptoms (multi select) ─────────────────────────────────────────
document.getElementById("symptomGrid").addEventListener("change", () => {
state.symptoms = [...form.querySelectorAll('input[name="symptom"]:checked')].map((i) => i.value);
if (state.symptoms.length > 0) navHint.textContent = "";
});
// ── Step 3 — duration & severity ─────────────────────────────────────────────
document.getElementById("durationGrid").addEventListener("change", (e) => {
state.duration = e.target.value;
});
const severity = document.getElementById("severity");
const sevValue = document.getElementById("sevValue");
severity.addEventListener("input", () => {
state.severity = Number(severity.value);
sevValue.textContent = severity.value;
});
// ── Navigation ───────────────────────────────────────────────────────────────
nextBtn.addEventListener("click", () => {
const error = validateStep();
if (error) {
navHint.textContent = error;
return;
}
if (state.step === TOTAL_STEPS - 1) renderResult();
state.step = Math.min(state.step + 1, TOTAL_STEPS);
render();
});
backBtn.addEventListener("click", () => {
state.step = Math.max(state.step - 1, 1);
render();
});
document.getElementById("bookBtn").addEventListener("click", () => {
document.getElementById("bookBtn").textContent = "Appointment requested ✓";
document.getElementById("bookBtn").disabled = true;
});
restartBtn.addEventListener("click", () => {
form.reset();
state.step = 1;
state.area = null;
state.symptoms = [];
state.duration = "<24h";
state.severity = 5;
sevValue.textContent = "5";
for (const c of document.getElementById("areaGrid").children) {
c.setAttribute("aria-checked", "false");
}
const bookBtn = document.getElementById("bookBtn");
bookBtn.textContent = "Book an appointment";
bookBtn.disabled = false;
render();
});
render();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Symptom Checker · Northbridge Clinic</title>
</head>
<body>
<header class="appbar">
<a class="brand" href="#">
<span class="brand-mark" aria-hidden="true">✚</span>
<span class="brand-name">Northbridge <em>Health</em></span>
</a>
<span class="appbar-tag">Symptom checker</span>
</header>
<main class="wizard">
<div class="progress">
<div class="progress-row">
<p class="progress-label" id="stepLabel">Step 1 of 4</p>
<p class="progress-name" id="stepName">Body area</p>
</div>
<div
class="progress-track"
role="progressbar"
aria-valuemin="1"
aria-valuemax="4"
aria-valuenow="1"
id="progressBar"
>
<span class="progress-fill" id="progressFill"></span>
</div>
</div>
<form class="card" id="wizardForm">
<!-- Step 1 — Body area -->
<fieldset class="step is-active" data-step="1">
<legend class="step-h">Where are your symptoms?</legend>
<p class="step-sub">Pick the area that fits best.</p>
<div class="area-grid" id="areaGrid" role="radiogroup" aria-label="Body area">
<button type="button" class="area-card" data-area="Head" role="radio" aria-checked="false">
<span class="area-ic" aria-hidden="true">🧠</span><span>Head</span>
</button>
<button type="button" class="area-card" data-area="Chest" role="radio" aria-checked="false">
<span class="area-ic" aria-hidden="true">🫁</span><span>Chest</span>
</button>
<button type="button" class="area-card" data-area="Abdomen" role="radio" aria-checked="false">
<span class="area-ic" aria-hidden="true">🩺</span><span>Abdomen</span>
</button>
<button type="button" class="area-card" data-area="Back" role="radio" aria-checked="false">
<span class="area-ic" aria-hidden="true">🧍</span><span>Back</span>
</button>
<button type="button" class="area-card" data-area="Limbs" role="radio" aria-checked="false">
<span class="area-ic" aria-hidden="true">🦵</span><span>Limbs</span>
</button>
<button type="button" class="area-card" data-area="Skin" role="radio" aria-checked="false">
<span class="area-ic" aria-hidden="true">🩹</span><span>Skin</span>
</button>
<button type="button" class="area-card" data-area="General" role="radio" aria-checked="false">
<span class="area-ic" aria-hidden="true">🌡️</span><span>General</span>
</button>
</div>
</fieldset>
<!-- Step 2 — Symptoms -->
<fieldset class="step" data-step="2">
<legend class="step-h">What are you feeling?</legend>
<p class="step-sub">Select all that apply — at least one.</p>
<div class="chip-grid" id="symptomGrid">
<label class="chip"><input type="checkbox" name="symptom" value="Fever" /><span>Fever</span></label>
<label class="chip"><input type="checkbox" name="symptom" value="Cough" /><span>Cough</span></label>
<label class="chip"><input type="checkbox" name="symptom" value="Headache" /><span>Headache</span></label>
<label class="chip"><input type="checkbox" name="symptom" value="Fatigue" /><span>Fatigue</span></label>
<label class="chip"><input type="checkbox" name="symptom" value="Nausea" /><span>Nausea</span></label>
<label class="chip"><input type="checkbox" name="symptom" value="Shortness of breath" /><span>Shortness of breath</span></label>
<label class="chip"><input type="checkbox" name="symptom" value="Pain" /><span>Pain</span></label>
<label class="chip"><input type="checkbox" name="symptom" value="Dizziness" /><span>Dizziness</span></label>
</div>
</fieldset>
<!-- Step 3 — Duration & severity -->
<fieldset class="step" data-step="3">
<legend class="step-h">How long and how bad?</legend>
<p class="step-sub">This helps shape the guidance.</p>
<div class="field">
<p class="field-label">Duration</p>
<div class="radio-grid" id="durationGrid">
<label class="radio"><input type="radio" name="duration" value="<24h" checked /><span>Less than 24h</span></label>
<label class="radio"><input type="radio" name="duration" value="1-3 days" /><span>1–3 days</span></label>
<label class="radio"><input type="radio" name="duration" value="4-7 days" /><span>4–7 days</span></label>
<label class="radio"><input type="radio" name="duration" value=">1 week" /><span>More than a week</span></label>
</div>
</div>
<div class="field">
<p class="field-label">
Severity <span class="sev-value" id="sevValue">5</span><span class="sev-of">/ 10</span>
</p>
<input
type="range"
min="1"
max="10"
value="5"
class="slider"
id="severity"
aria-label="Severity from 1 to 10"
/>
<div class="slider-scale"><span>Mild</span><span>Moderate</span><span>Severe</span></div>
</div>
</fieldset>
<!-- Step 4 — Result -->
<fieldset class="step" data-step="4">
<legend class="step-h">Your illustrative result</legend>
<div class="result" id="result" role="status" aria-live="polite">
<div class="result-banner" id="resultBanner">
<span class="rb-level" id="rbLevel">—</span>
<span class="rb-rec" id="rbRec"></span>
</div>
<ul class="next-steps" id="nextSteps"></ul>
<div class="disclaimer" role="note">
<strong>This is not a diagnosis.</strong> This tool is illustrative only and cannot
assess your health. If you feel unwell, contact a healthcare professional. In an
emergency, call your local emergency number.
</div>
<button type="button" class="btn primary block" id="bookBtn">Book an appointment</button>
</div>
</fieldset>
<!-- Nav -->
<div class="nav">
<button type="button" class="btn ghost" id="backBtn" hidden>Back</button>
<p class="nav-hint" id="navHint" role="status" aria-live="polite"></p>
<button type="button" class="btn primary" id="nextBtn">Next</button>
<button type="button" class="btn ghost" id="restartBtn" hidden>Start over</button>
</div>
</form>
</main>
<script src="script.js"></script>
</body>
</html>Symptom Checker Wizard
A calm, clinical-white wizard that walks a patient through four steps in teal and soft coral. A top progress bar tracks “Step 2 of 4” with a fill that grows as you advance. Step one is a grid of selectable body-area cards; step two is a multi-select set of symptom chips; step three captures how long it’s been going on and how severe it feels with a 1–10 slider. The final step computes an illustrative triage level deterministically from the inputs — urgent care, GP appointment, or self-care — and shows a colored banner, a recommendation, next steps, a booking CTA, and a start-over control. All logic is vanilla JS with no randomness.
Illustrative UI only — not a diagnostic tool and not intended for real medical use.