Clinic — Patient Intake / Triage Form
A single-page patient intake and triage form with sectioned cards for personal details, reason for visit and health history, a sticky four-step progress indicator that tracks as you scroll, a live colour-coded symptom-severity slider, add-and-remove medication and allergy chips driven by Enter, an accessible consent checkbox, inline validation that glows the required fields on an invalid submit, and a review summary card plus success toast on completion.
MCP
Codice
: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;
--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:
radial-gradient(1200px 480px at 50% -8%, rgba(18, 156, 147, 0.1), transparent 60%),
var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
:focus-visible {
outline: 3px solid rgba(18, 156, 147, 0.45);
outline-offset: 2px;
border-radius: 4px;
}
/* ── Shell ── */
.intake {
max-width: 680px;
margin: 0 auto;
padding: 40px 20px 64px;
display: flex;
flex-direction: column;
gap: 18px;
}
/* ── Header ── */
.intake-head {
display: flex;
flex-direction: column;
gap: 12px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand-mark {
width: 42px;
height: 42px;
display: grid;
place-items: center;
border-radius: 12px;
background: linear-gradient(150deg, var(--teal), var(--teal-700));
color: #fff;
font-weight: 800;
font-size: 1.2rem;
box-shadow: var(--shadow-1);
}
.brand-name {
font-weight: 700;
font-size: 0.96rem;
letter-spacing: -0.01em;
}
.brand-sub {
font-size: 0.78rem;
color: var(--muted);
}
.intake-head h1 {
font-size: 1.7rem;
font-weight: 800;
letter-spacing: -0.02em;
margin-top: 4px;
}
.lede {
color: var(--ink-2);
font-size: 0.94rem;
max-width: 56ch;
}
/* ── Progress ── */
.progress {
position: sticky;
top: 0;
z-index: 20;
margin-top: 6px;
padding: 12px 14px 14px;
background: rgba(241, 247, 246, 0.86);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--shadow-1);
}
.steps {
list-style: none;
display: flex;
justify-content: space-between;
gap: 6px;
}
.step {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.82rem;
font-weight: 600;
color: var(--muted);
transition: color 0.2s;
min-width: 0;
}
.step .dot {
flex-shrink: 0;
width: 24px;
height: 24px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--white);
border: 1.5px solid var(--line-2);
font-size: 0.78rem;
font-weight: 700;
transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.step.is-active {
color: var(--teal-700);
}
.step.is-active .dot {
background: var(--teal-d);
border-color: var(--teal-d);
color: #fff;
}
.step.is-done .dot {
background: var(--teal-50);
border-color: var(--teal);
color: var(--teal-d);
}
.step.is-done .dot::after {
content: "✓";
}
.step.is-done .dot {
font-size: 0;
}
.step.is-done .dot::after {
font-size: 0.8rem;
}
.bar {
margin-top: 12px;
height: 6px;
border-radius: 999px;
background: var(--line);
overflow: hidden;
}
.bar > span {
display: block;
height: 100%;
width: 25%;
border-radius: 999px;
background: linear-gradient(90deg, var(--teal), var(--teal-d));
transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
/* ── Form & cards ── */
.form {
display: flex;
flex-direction: column;
gap: 16px;
}
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: var(--shadow-1);
scroll-margin-top: 96px;
}
.card-head {
margin-bottom: 18px;
}
.card-head h2 {
font-size: 1.12rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.card-sub {
font-size: 0.86rem;
color: var(--muted);
margin-top: 2px;
}
/* ── Fields ── */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.field-wide {
grid-column: 1 / -1;
}
.field + .field {
margin-top: 16px;
}
label {
font-size: 0.84rem;
font-weight: 600;
color: var(--ink-2);
}
label em {
color: var(--danger);
font-style: normal;
font-weight: 700;
}
label .opt,
label .quick-label {
font-weight: 500;
color: var(--muted);
}
input[type="text"],
input[type="tel"],
input[type="email"],
input[type="date"],
textarea {
width: 100%;
font: inherit;
font-size: 0.92rem;
color: var(--ink);
background: #fbfdfc;
border: 1.5px solid var(--line-2);
border-radius: var(--r-sm);
padding: 11px 13px;
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}
input::placeholder,
textarea::placeholder {
color: #9bafab;
}
input:hover,
textarea:hover {
border-color: rgba(18, 156, 147, 0.5);
}
input:focus,
textarea:focus {
outline: none;
background: var(--white);
border-color: var(--teal);
box-shadow: 0 0 0 4px rgba(18, 156, 147, 0.14);
}
textarea {
resize: vertical;
min-height: 96px;
}
/* invalid glow */
.field.invalid input,
.field.invalid textarea,
.field.invalid .chip-input {
border-color: var(--danger);
background: rgba(212, 80, 62, 0.04);
box-shadow: 0 0 0 4px rgba(212, 80, 62, 0.12);
animation: nudge 0.3s ease;
}
@keyframes nudge {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}
.hint {
font-size: 0.78rem;
color: var(--muted);
}
.hint.err {
display: none;
color: var(--danger);
font-weight: 600;
}
.field.invalid .hint.err {
display: block;
}
.textarea-foot {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 10px;
}
.counter {
font-size: 0.74rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* ── Severity slider ── */
.sev-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.sev-pill {
font-size: 0.78rem;
font-weight: 700;
padding: 4px 12px;
border-radius: 999px;
background: var(--sev-bg, var(--teal-50));
color: var(--sev-fg, var(--teal-d));
transition: background 0.2s, color 0.2s;
white-space: nowrap;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 8px;
margin: 14px 0 8px;
border-radius: 999px;
background: linear-gradient(
90deg,
var(--sev-track, var(--teal)) var(--sev-pct, 50%),
var(--line) var(--sev-pct, 50%)
);
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--sev-track, var(--teal));
box-shadow: var(--shadow-1);
transition: transform 0.12s, border-color 0.2s;
}
input[type="range"]::-webkit-slider-thumb:active {
transform: scale(1.14);
}
input[type="range"]::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--sev-track, var(--teal));
box-shadow: var(--shadow-1);
}
input[type="range"]:focus-visible {
outline: none;
}
input[type="range"]:focus-visible::-webkit-slider-thumb {
box-shadow: 0 0 0 5px rgba(18, 156, 147, 0.22);
}
.sev-scale {
display: flex;
justify-content: space-between;
font-size: 0.72rem;
color: var(--muted);
font-weight: 500;
}
/* ── Chip inputs ── */
.chip-input {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 9px 10px;
background: #fbfdfc;
border: 1.5px solid var(--line-2);
border-radius: var(--r-sm);
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}
.chip-input:focus-within {
background: var(--white);
border-color: var(--teal);
box-shadow: 0 0 0 4px rgba(18, 156, 147, 0.14);
}
.chip-input.danger-input:focus-within {
border-color: var(--coral);
box-shadow: 0 0 0 4px rgba(255, 122, 102, 0.18);
}
.chips {
display: contents;
list-style: none;
}
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 6px 5px 12px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 600;
background: var(--teal-50);
color: var(--teal-700);
animation: pop 0.18s ease;
}
.danger-input .chip {
background: var(--coral-soft);
color: #b8412e;
}
@keyframes pop {
from { transform: scale(0.8); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.chip button {
border: none;
cursor: pointer;
width: 18px;
height: 18px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(16, 50, 47, 0.1);
color: inherit;
font-size: 0.9rem;
line-height: 1;
transition: background 0.15s;
}
.chip button:hover {
background: rgba(16, 50, 47, 0.22);
}
.chip-entry {
flex: 1;
min-width: 130px;
border: none !important;
background: transparent !important;
box-shadow: none !important;
padding: 5px 4px !important;
font-size: 0.88rem;
}
.chip-entry:focus {
outline: none;
}
.quick-add {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-top: 4px;
}
.quick-label {
font-size: 0.76rem;
color: var(--muted);
font-weight: 600;
}
.quick {
border: 1px solid var(--line-2);
background: var(--white);
border-radius: 999px;
padding: 4px 11px;
font: inherit;
font-size: 0.78rem;
font-weight: 600;
color: var(--ink-2);
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s, opacity 0.15s;
}
.quick:hover {
background: var(--coral-soft);
border-color: var(--coral);
color: #b8412e;
}
.quick[hidden] {
display: none;
}
/* ── Consent ── */
.consent {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 16px;
border: 1.5px solid var(--line-2);
border-radius: var(--r-md);
background: #fbfdfc;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.consent:hover {
border-color: var(--teal);
background: var(--teal-50);
}
.consent input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.consent-box {
flex-shrink: 0;
width: 22px;
height: 22px;
margin-top: 1px;
border: 2px solid var(--line-2);
border-radius: 6px;
background: #fff;
display: grid;
place-items: center;
transition: background 0.15s, border-color 0.15s;
}
.consent-box::after {
content: "";
width: 11px;
height: 6px;
border-left: 2.4px solid #fff;
border-bottom: 2.4px solid #fff;
transform: rotate(-45deg) scale(0);
transform-origin: center;
margin-top: -2px;
transition: transform 0.15s ease;
}
.consent input:checked + .consent-box {
background: var(--teal-d);
border-color: var(--teal-d);
}
.consent input:checked + .consent-box::after {
transform: rotate(-45deg) scale(1);
}
.consent input:focus-visible + .consent-box {
box-shadow: 0 0 0 4px rgba(18, 156, 147, 0.22);
}
.consent-text {
font-size: 0.88rem;
color: var(--ink-2);
line-height: 1.45;
}
.consent-text em {
color: var(--danger);
font-style: normal;
font-weight: 700;
}
#sec-consent .hint.err {
margin-top: 8px;
}
#sec-consent.invalid .consent {
border-color: var(--danger);
box-shadow: 0 0 0 4px rgba(212, 80, 62, 0.1);
}
#sec-consent.invalid .hint.err {
display: block;
}
/* ── Footer / submit ── */
.form-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding-top: 4px;
}
.reassure {
display: flex;
align-items: center;
gap: 7px;
font-size: 0.8rem;
color: var(--muted);
font-weight: 500;
}
.reassure svg {
color: var(--teal);
flex-shrink: 0;
}
.btn {
border: none;
border-radius: 12px;
padding: 12px 22px;
font: inherit;
font-weight: 700;
font-size: 0.9rem;
cursor: pointer;
transition: transform 0.12s, background 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.btn:active {
transform: translateY(1px);
}
.btn.primary {
background: var(--teal-d);
color: #fff;
box-shadow: 0 6px 18px rgba(12, 122, 115, 0.28);
}
.btn.primary:hover {
background: var(--teal-700);
box-shadow: 0 8px 22px rgba(12, 122, 115, 0.34);
}
.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);
color: var(--teal-d);
}
/* ── Review summary ── */
.review {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 24px;
box-shadow: var(--shadow-2);
animation: rise 0.3s ease;
}
.review[hidden] {
display: none;
}
@keyframes rise {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.review-head {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
}
.review-tick {
flex-shrink: 0;
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(47, 158, 111, 0.14);
color: var(--ok);
}
.review-head h2 {
font-size: 1.2rem;
font-weight: 800;
letter-spacing: -0.01em;
}
.review-sub {
font-size: 0.86rem;
color: var(--muted);
}
.review-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: var(--line);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
}
.review-grid .row {
background: var(--white);
padding: 14px 16px;
}
.review-grid .row.full {
grid-column: 1 / -1;
}
.review-grid dt {
font-size: 0.74rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
margin-bottom: 4px;
}
.review-grid dd {
font-size: 0.92rem;
font-weight: 600;
color: var(--ink);
word-break: break-word;
}
.review-grid dd.empty {
color: var(--muted);
font-weight: 500;
font-style: italic;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 2px;
}
.tag {
font-size: 0.78rem;
font-weight: 600;
padding: 3px 10px;
border-radius: 999px;
background: var(--teal-50);
color: var(--teal-700);
}
.tag.danger {
background: var(--coral-soft);
color: #b8412e;
}
.sev-chip {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 0.84rem;
font-weight: 700;
}
.sev-chip .swatch {
width: 12px;
height: 12px;
border-radius: 50%;
}
.review-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
/* ── Toast ── */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
background: var(--ink);
color: #fff;
padding: 13px 20px;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow-2);
z-index: 50;
max-width: 90vw;
display: flex;
align-items: center;
gap: 9px;
}
.toast::before {
content: "✓";
display: grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--ok);
color: #fff;
font-size: 0.74rem;
font-weight: 800;
flex-shrink: 0;
}
/* ── Responsive ── */
@media (max-width: 520px) {
.intake {
padding: 24px 14px 48px;
}
.intake-head h1 {
font-size: 1.42rem;
}
.grid-2 {
grid-template-columns: 1fr;
}
.field.field-wide {
grid-column: auto;
}
.step .label {
display: none;
}
.steps {
justify-content: space-around;
}
.review-grid {
grid-template-columns: 1fr;
}
.review-grid .row.full {
grid-column: auto;
}
.form-foot {
flex-direction: column-reverse;
align-items: stretch;
}
.form-foot .btn.primary {
width: 100%;
}
.review-actions .btn {
flex: 1;
}
}// ── Toast ──────────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2800);
}
const form = document.getElementById("intake-form");
// ── Reason character counter ──────────────────────────────────────────────
const reason = document.getElementById("reason");
const reasonCount = document.getElementById("reason-count");
reason.addEventListener("input", () => {
reasonCount.textContent = reason.value.length;
});
// ── Severity slider (live label + colour) ─────────────────────────────────
const severity = document.getElementById("severity");
const sevPill = document.getElementById("sev-pill");
const SEV_BANDS = [
{ max: 3, label: "Mild", track: "#2f9e6f", bg: "rgba(47,158,111,0.14)", fg: "#2f9e6f" },
{ max: 6, label: "Moderate", track: "#d98a2b", bg: "rgba(217,138,43,0.16)", fg: "#b9741f" },
{ max: 8, label: "High", track: "#ff7a66", bg: "#ffe6df", fg: "#b8412e" },
{ max: 10, label: "Severe", track: "#d4503e", bg: "rgba(212,80,62,0.14)", fg: "#d4503e" },
];
function bandFor(v) {
return SEV_BANDS.find((b) => v <= b.max);
}
function paintSeverity() {
const v = Number(severity.value);
const band = bandFor(v);
const pct = ((v - 1) / 9) * 100;
severity.style.setProperty("--sev-pct", pct + "%");
severity.style.setProperty("--sev-track", band.track);
sevPill.style.setProperty("--sev-bg", band.bg);
sevPill.style.setProperty("--sev-fg", band.fg);
sevPill.textContent = v + " · " + band.label;
}
severity.addEventListener("input", paintSeverity);
paintSeverity();
// ── Chip inputs (meds + allergies) ────────────────────────────────────────
function makeChipGroup(inputId, listId, opts) {
opts = opts || {};
const input = document.getElementById(inputId);
const list = document.getElementById(listId);
const items = [];
function render() {
list.innerHTML = "";
items.forEach((text, i) => {
const li = document.createElement("li");
li.className = "chip";
const span = document.createElement("span");
span.textContent = text;
const x = document.createElement("button");
x.type = "button";
x.setAttribute("aria-label", "Remove " + text);
x.textContent = "×";
x.addEventListener("click", () => {
items.splice(i, 1);
render();
if (opts.onChange) opts.onChange(items);
});
li.append(span, x);
list.appendChild(li);
});
if (opts.onChange) opts.onChange(items);
}
function add(raw) {
const text = raw.trim().replace(/\s+/g, " ");
if (!text) return false;
const dup = items.some((t) => t.toLowerCase() === text.toLowerCase());
if (dup) {
showToast('"' + text + '" is already on the list.');
return false;
}
items.push(text);
render();
return true;
}
input.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
if (add(input.value)) input.value = "";
} else if (e.key === "Backspace" && input.value === "" && items.length) {
items.pop();
render();
}
});
input.addEventListener("blur", () => {
if (add(input.value)) input.value = "";
});
return { items, add: (t) => { if (add(t)) return true; return false; }, focus: () => input.focus() };
}
const meds = makeChipGroup("med-input", "meds-chips");
const allergies = makeChipGroup("allergy-input", "allergies-chips", {
onChange(items) {
// Hide quick-add buttons that are already present.
document.querySelectorAll("#allergy-quick .quick").forEach((b) => {
const taken = items.some((t) => t.toLowerCase() === b.dataset.add.toLowerCase());
b.hidden = taken;
});
},
});
document.getElementById("allergy-quick").addEventListener("click", (e) => {
const b = e.target.closest(".quick");
if (!b) return;
allergies.add(b.dataset.add);
allergies.focus();
});
// ── Progress / active-section tracking ────────────────────────────────────
const sections = [
{ id: "sec-about", step: "about" },
{ id: "sec-visit", step: "visit" },
{ id: "sec-history", step: "history" },
{ id: "sec-consent", step: "consent" },
];
const stepEls = {};
document.querySelectorAll(".step").forEach((el) => (stepEls[el.dataset.step] = el));
const barFill = document.getElementById("bar-fill");
function setActiveStep(idx) {
sections.forEach((s, i) => {
const el = stepEls[s.step];
el.classList.toggle("is-active", i === idx);
el.classList.toggle("is-done", i < idx);
});
barFill.style.width = ((idx + 1) / sections.length) * 100 + "%";
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const idx = sections.findIndex((s) => s.id === entry.target.id);
if (idx > -1) setActiveStep(idx);
}
});
},
{ rootMargin: "-45% 0px -45% 0px", threshold: 0 }
);
sections.forEach((s) => observer.observe(document.getElementById(s.id)));
// ── Inline validation ─────────────────────────────────────────────────────
function fieldOf(input) {
return input.closest(".field");
}
function clearError(input) {
const f = fieldOf(input);
if (f) f.classList.remove("invalid");
}
form.querySelectorAll("input, textarea").forEach((el) => {
el.addEventListener("input", () => clearError(el));
});
document.getElementById("consent").addEventListener("change", () => {
document.getElementById("sec-consent").classList.remove("invalid");
});
function validate() {
let firstBad = null;
const fail = (el, container) => {
(container || fieldOf(el)).classList.add("invalid");
if (!firstBad) firstBad = container || el;
};
["firstName", "lastName", "dob", "phone", "reason"].forEach((id) => {
const el = document.getElementById(id);
if (!el.value.trim()) fail(el);
});
const email = document.getElementById("email");
if (email.value.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim())) {
fail(email);
}
const consentSec = document.getElementById("sec-consent");
if (!document.getElementById("consent").checked) fail(null, consentSec);
return firstBad;
}
// ── Submit → review summary ───────────────────────────────────────────────
function val(id) {
return document.getElementById(id).value.trim();
}
function fmtDate(iso) {
if (!iso) return "";
const d = new Date(iso + "T00:00:00");
if (isNaN(d)) return iso;
return d.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
}
function buildReview() {
const grid = document.getElementById("review-grid");
const sevVal = Number(severity.value);
const band = bandFor(sevVal);
const tags = (items, danger) =>
items.length
? '<div class="tag-row">' +
items
.map((t) => '<span class="tag' + (danger ? " danger" : "") + '">' + esc(t) + "</span>")
.join("") +
"</div>"
: '<dd class="empty">None listed</dd>';
const rows = [
{ dt: "Name", dd: esc(val("firstName") + " " + val("lastName")) },
{ dt: "Date of birth", dd: esc(fmtDate(val("dob"))) },
{ dt: "Mobile phone", dd: esc(val("phone")) },
{ dt: "Email", dd: val("email") ? esc(val("email")) : '<span class="empty">Not provided</span>' },
{
dt: "Reason for visit",
dd: esc(val("reason")),
full: true,
},
{
dt: "Symptom severity",
dd:
'<span class="sev-chip"><span class="swatch" style="background:' +
band.track +
'"></span>' +
sevVal +
" / 10 · " +
band.label +
"</span>",
},
{ dt: "Consent", dd: "Confirmed" },
{ dt: "Current medications", ddHtml: tags(meds.items, false), full: true },
{ dt: "Known allergies", ddHtml: tags(allergies.items, true), full: true },
];
grid.innerHTML = rows
.map((r) => {
const body = r.ddHtml
? (r.ddHtml.startsWith("<dd") ? r.ddHtml : "<dd>" + r.ddHtml + "</dd>")
: "<dd>" + r.dd + "</dd>";
return '<div class="row' + (r.full ? " full" : "") + '"><dt>' + r.dt + "</dt>" + body + "</div>";
})
.join("");
}
function esc(s) {
return String(s).replace(/[&<>"']/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])
);
}
const review = document.getElementById("review");
form.addEventListener("submit", (e) => {
e.preventDefault();
const bad = validate();
if (bad) {
showToast("Please complete the highlighted fields.");
bad.scrollIntoView({ behavior: "smooth", block: "center" });
const focusable = bad.querySelector("input, textarea") || bad;
if (focusable.focus) setTimeout(() => focusable.focus(), 300);
return;
}
buildReview();
form.hidden = true;
review.hidden = false;
setActiveStep(sections.length - 1);
review.scrollIntoView({ behavior: "smooth", block: "start" });
showToast("Intake submitted — your care team has it.");
});
document.getElementById("edit-btn").addEventListener("click", () => {
review.hidden = true;
form.hidden = false;
document.getElementById("sec-about").scrollIntoView({ behavior: "smooth", block: "start" });
});
document.getElementById("done-btn").addEventListener("click", () => {
showToast("All set — see you at your appointment.");
});<!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>Patient Intake · Northpoint Clinic</title>
</head>
<body>
<main class="intake">
<header class="intake-head">
<div class="brand">
<div class="brand-mark" aria-hidden="true">N</div>
<div class="brand-text">
<p class="brand-name">Northpoint Clinic</p>
<p class="brand-sub">Primary care · Triage intake</p>
</div>
</div>
<h1>Before your visit</h1>
<p class="lede">
Tell us a little about why you are here today. This helps Dr. Lena Okafor
and the care team prepare for your appointment. It only takes a few minutes.
</p>
<nav class="progress" aria-label="Form sections">
<ol class="steps" id="steps">
<li class="step is-active" data-step="about">
<span class="dot">1</span><span class="label">About you</span>
</li>
<li class="step" data-step="visit">
<span class="dot">2</span><span class="label">Your visit</span>
</li>
<li class="step" data-step="history">
<span class="dot">3</span><span class="label">Health history</span>
</li>
<li class="step" data-step="consent">
<span class="dot">4</span><span class="label">Consent</span>
</li>
</ol>
<div class="bar" aria-hidden="true"><span id="bar-fill"></span></div>
</nav>
</header>
<form class="form" id="intake-form" novalidate>
<!-- ── Section 1 · Personal info ── -->
<section class="card" id="sec-about" aria-labelledby="h-about">
<div class="card-head">
<h2 id="h-about">About you</h2>
<p class="card-sub">Your contact and basic details.</p>
</div>
<div class="grid-2">
<div class="field">
<label for="firstName">First name <em>*</em></label>
<input id="firstName" name="firstName" type="text" required
autocomplete="given-name" placeholder="Maya" />
<span class="hint err" data-for="firstName">Please enter your first name.</span>
</div>
<div class="field">
<label for="lastName">Last name <em>*</em></label>
<input id="lastName" name="lastName" type="text" required
autocomplete="family-name" placeholder="Bloom" />
<span class="hint err" data-for="lastName">Please enter your last name.</span>
</div>
<div class="field">
<label for="dob">Date of birth <em>*</em></label>
<input id="dob" name="dob" type="date" required autocomplete="bday" />
<span class="hint err" data-for="dob">A date of birth is required.</span>
</div>
<div class="field">
<label for="phone">Mobile phone <em>*</em></label>
<input id="phone" name="phone" type="tel" required
autocomplete="tel" inputmode="tel" placeholder="(555) 248-1190" />
<span class="hint err" data-for="phone">Enter a phone number we can reach you on.</span>
</div>
<div class="field field-wide">
<label for="email">Email <span class="opt">(optional)</span></label>
<input id="email" name="email" type="email"
autocomplete="email" placeholder="[email protected]" />
<span class="hint err" data-for="email">That email address doesn't look right.</span>
</div>
</div>
</section>
<!-- ── Section 2 · Reason + severity ── -->
<section class="card" id="sec-visit" aria-labelledby="h-visit">
<div class="card-head">
<h2 id="h-visit">Your visit</h2>
<p class="card-sub">Help us understand what's bringing you in.</p>
</div>
<div class="field">
<label for="reason">Reason for visit <em>*</em></label>
<textarea id="reason" name="reason" rows="4" required maxlength="600"
placeholder="Describe your main symptom or concern — when it started, how it feels, and anything that makes it better or worse."></textarea>
<div class="textarea-foot">
<span class="hint err" data-for="reason">Please tell us briefly why you're here.</span>
<span class="counter"><span id="reason-count">0</span> / 600</span>
</div>
</div>
<div class="field">
<div class="sev-head">
<label for="severity">How severe is it right now?</label>
<span class="sev-pill" id="sev-pill" aria-live="polite">5 · Moderate</span>
</div>
<input id="severity" name="severity" type="range" min="1" max="10" step="1"
value="5" aria-describedby="sev-pill" />
<div class="sev-scale" aria-hidden="true">
<span>1 · Mild</span><span>5 · Moderate</span><span>10 · Severe</span>
</div>
</div>
</section>
<!-- ── Section 3 · Medications + allergies ── -->
<section class="card" id="sec-history" aria-labelledby="h-history">
<div class="card-head">
<h2 id="h-history">Health history</h2>
<p class="card-sub">Add any current medications and known allergies.</p>
</div>
<div class="field">
<label for="med-input">Current medications</label>
<div class="chip-input" data-group="meds">
<ul class="chips" id="meds-chips" aria-label="Current medications"></ul>
<input id="med-input" type="text" class="chip-entry"
placeholder="Type a medication, press Enter"
aria-describedby="meds-help" autocomplete="off" />
</div>
<span class="hint" id="meds-help">e.g. Lisinopril 10mg · Metformin · Multivitamin</span>
</div>
<div class="field">
<label for="allergy-input">Known allergies</label>
<div class="chip-input danger-input" data-group="allergies">
<ul class="chips" id="allergies-chips" aria-label="Known allergies"></ul>
<input id="allergy-input" type="text" class="chip-entry"
placeholder="Type an allergy, press Enter"
aria-describedby="allergies-help" autocomplete="off" />
</div>
<div class="quick-add" id="allergy-quick">
<span class="quick-label">Common:</span>
<button type="button" class="quick" data-add="Penicillin">Penicillin</button>
<button type="button" class="quick" data-add="Peanuts">Peanuts</button>
<button type="button" class="quick" data-add="Latex">Latex</button>
<button type="button" class="quick" data-add="Ibuprofen">Ibuprofen</button>
</div>
<span class="hint" id="allergies-help">Leave blank if you have no known allergies.</span>
</div>
</section>
<!-- ── Section 4 · Consent ── -->
<section class="card" id="sec-consent" aria-labelledby="h-consent">
<div class="card-head">
<h2 id="h-consent">Consent</h2>
<p class="card-sub">One last thing before we begin.</p>
</div>
<label class="consent" for="consent">
<input id="consent" name="consent" type="checkbox" required />
<span class="consent-box" aria-hidden="true"></span>
<span class="consent-text">
I confirm the information above is accurate and I consent to Northpoint
Clinic using it to prepare for and provide my care.
<em>*</em>
</span>
</label>
<span class="hint err" data-for="consent">Please confirm to continue.</span>
</section>
<div class="form-foot">
<p class="reassure">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path fill="currentColor" d="M12 2 4 5v6c0 5 3.4 9.3 8 11 4.6-1.7 8-6 8-11V5l-8-3Zm-1 14-4-4 1.4-1.4L11 13.2l4.6-4.6L17 10l-6 6Z"/>
</svg>
Your answers are kept private to your care team.
</p>
<button type="submit" class="btn primary" id="submit">Submit intake</button>
</div>
</form>
<!-- ── Review summary (revealed on submit) ── -->
<section class="review" id="review" hidden aria-live="polite" aria-labelledby="review-h">
<div class="review-head">
<div class="review-tick" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22">
<path fill="none" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" stroke-linejoin="round" d="m5 13 4 4L19 7"/>
</svg>
</div>
<div>
<h2 id="review-h">Intake received</h2>
<p class="review-sub">Thank you — please review what you shared below.</p>
</div>
</div>
<dl class="review-grid" id="review-grid"></dl>
<div class="review-actions">
<button type="button" class="btn ghost" id="edit-btn">Edit answers</button>
<button type="button" class="btn primary" id="done-btn">Done</button>
</div>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Patient Intake / Triage Form
A calm, single-page intake flow for Northpoint Clinic that gathers everything the care team needs before a visit. Four sectioned cards walk the patient through About you, Your visit, Health history and Consent, while a sticky progress indicator at the top tracks the active section live as you scroll and fills a thin teal bar to show how far along you are.
The Your visit card pairs a character-counted reason textarea with a symptom-severity slider that updates a live label and shifts colour from green through amber to coral-red as the rating climbs from Mild to Severe. In Health history, current medications and known allergies are entered as chips — type and press Enter to add, click the × or press Backspace to remove, with duplicate guarding, quick-add buttons for common allergens and a gentle toast for feedback. Submitting validates the required fields inline, glowing any that are missing, then folds the form into a tidy review summary card and fires a success toast.
Illustrative UI only — not intended for real medical use.