Clinic — E-Prescription Pad
A clinician e-prescription pad with type-ahead drug autocomplete, dose / route / frequency / duration / quantity / refills fields, a live ℞ Rx preview that assembles the sig line as you type, and a running prescription list ready to send to pharmacy.
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);
}
.patient-chip {
display: flex;
align-items: center;
gap: 11px;
background: var(--teal-50);
border: 1px solid var(--line);
border-radius: 999px;
padding: 6px 14px 6px 6px;
}
.pc-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--teal-d);
color: #fff;
font-weight: 700;
font-size: 0.78rem;
}
.pc-info {
display: flex;
flex-direction: column;
}
.pc-info strong {
font-size: 0.88rem;
}
.pc-info small {
color: var(--muted);
font-size: 0.74rem;
}
/* ── Layout ── */
.pad {
max-width: 1020px;
margin: 0 auto;
padding: 28px 28px 56px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
align-items: start;
}
.pane {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 22px 24px;
}
.pane-head {
margin-bottom: 18px;
}
.preview-pane {
display: flex;
flex-direction: column;
}
.pane-head h1 {
font-size: 1.4rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.sub {
color: var(--muted);
font-size: 0.9rem;
margin-top: 3px;
}
.section-h {
font-size: 1.02rem;
font-weight: 700;
letter-spacing: -0.01em;
}
/* ── Form ── */
.rx-form {
display: flex;
flex-direction: column;
gap: 14px;
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field label {
font-size: 0.78rem;
font-weight: 600;
color: var(--ink-2);
letter-spacing: 0.01em;
}
.field input,
.field select,
.field textarea {
font: inherit;
font-size: 0.9rem;
color: var(--ink);
background: var(--white);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 10px 12px;
width: 100%;
transition: border-color 0.15s, box-shadow 0.15s;
}
.field textarea {
resize: vertical;
min-height: 56px;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
outline: none;
border-color: var(--teal);
box-shadow: 0 0 0 3px rgba(18, 156, 147, 0.16);
}
.field input.is-invalid,
.field select.is-invalid {
border-color: var(--danger);
box-shadow: 0 0 0 3px rgba(212, 80, 62, 0.14);
}
/* ── Autocomplete ── */
.combo {
position: relative;
}
.ac-list {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
list-style: none;
background: var(--white);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
box-shadow: var(--shadow-2);
max-height: 232px;
overflow-y: auto;
z-index: 30;
padding: 4px;
}
.ac-item {
padding: 9px 11px;
border-radius: 6px;
font-size: 0.88rem;
cursor: pointer;
display: flex;
justify-content: space-between;
gap: 10px;
}
.ac-item .ac-cat {
color: var(--muted);
font-size: 0.76rem;
}
.ac-item:hover,
.ac-item.is-active {
background: var(--teal-50);
color: var(--teal-700);
}
.ac-item mark {
background: var(--coral-soft);
color: var(--ink);
border-radius: 3px;
padding: 0 1px;
}
.ac-empty {
padding: 11px;
font-size: 0.84rem;
color: var(--muted);
text-align: center;
}
/* ── Checkbox ── */
.check {
display: inline-flex;
align-items: center;
gap: 9px;
font-size: 0.88rem;
font-weight: 500;
color: var(--ink-2);
cursor: pointer;
}
.check input {
width: 17px;
height: 17px;
accent-color: var(--teal);
cursor: pointer;
}
/* ── Buttons ── */
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 2px;
}
.btn {
border: none;
border-radius: 11px;
padding: 11px 18px;
font: inherit;
font-weight: 600;
font-size: 0.9rem;
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.ghost {
background: var(--teal-50);
color: var(--teal-700);
}
.btn.ghost:hover {
background: #d7ece9;
}
.btn.send {
width: 100%;
margin-top: 14px;
background: var(--teal-d);
color: #fff;
}
.btn.send:hover:not(:disabled) {
background: var(--teal-700);
}
.btn.send:disabled {
background: var(--line-2);
color: var(--white);
cursor: not-allowed;
}
/* ── Rx preview card ── */
.rx-card {
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 20px 22px;
background: linear-gradient(180deg, #fbfefd, var(--white));
box-shadow: var(--shadow-1);
}
.rx-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
border-bottom: 1px solid var(--line);
padding-bottom: 12px;
}
.rx-clinic {
font-weight: 700;
font-size: 0.98rem;
color: var(--teal-700);
}
.rx-clinic-meta {
font-size: 0.76rem;
color: var(--muted);
margin-top: 2px;
}
.rx-sym {
font-size: 2rem;
font-weight: 700;
line-height: 1;
color: var(--teal-d);
}
.rx-patient {
display: flex;
justify-content: space-between;
gap: 12px;
font-size: 0.82rem;
color: var(--ink-2);
font-weight: 600;
padding: 12px 0;
border-bottom: 1px dashed var(--line);
}
.rx-body {
padding: 14px 0;
min-height: 110px;
}
.rx-drug {
font-size: 1.18rem;
font-weight: 800;
letter-spacing: -0.01em;
color: var(--ink);
}
.rx-sig {
margin-top: 8px;
font-size: 0.92rem;
color: var(--ink-2);
line-height: 1.55;
}
.rx-notes {
margin-top: 8px;
font-size: 0.84rem;
color: var(--muted);
font-style: italic;
}
.rx-meta {
display: flex;
gap: 22px;
margin-top: 14px;
font-size: 0.84rem;
color: var(--ink-2);
}
.rx-meta strong {
color: var(--ink);
}
.rx-foot {
border-top: 1px solid var(--line);
padding-top: 12px;
}
.rx-signline {
display: block;
font-family: "Inter", cursive;
font-weight: 700;
font-size: 0.98rem;
color: var(--teal-700);
border-bottom: 1.5px solid var(--line-2);
padding-bottom: 4px;
margin-bottom: 5px;
width: 60%;
}
.rx-sign small {
font-size: 0.74rem;
color: var(--muted);
}
/* ── Prescription list ── */
.list-head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 22px 0 12px;
}
.count-pill {
font-size: 0.74rem;
font-weight: 700;
color: var(--teal-700);
background: var(--teal-50);
border-radius: 999px;
padding: 4px 11px;
}
.rx-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.rx-list .empty {
text-align: center;
color: var(--muted);
font-size: 0.86rem;
padding: 18px 0;
border: 1px dashed var(--line-2);
border-radius: var(--r-sm);
}
.rx-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 11px 13px;
background: var(--white);
}
.li-name {
font-weight: 700;
font-size: 0.9rem;
}
.li-sig {
font-size: 0.79rem;
color: var(--muted);
margin-top: 2px;
}
.remove-btn {
border: none;
background: transparent;
color: var(--muted);
font-size: 1rem;
line-height: 1;
cursor: pointer;
border-radius: 6px;
padding: 4px 7px;
transition: background 0.15s, color 0.15s;
}
.remove-btn:hover {
background: var(--coral-soft);
color: var(--danger);
}
/* ── 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;
}
.toast.is-error {
background: var(--danger);
}
@media (max-width: 820px) {
.pad {
grid-template-columns: 1fr;
}
.patient-chip .pc-info {
display: none;
}
}// ── Medication formulary (illustrative) ──────────────────────────────────────
const DRUGS = [
{ name: "Amoxicillin 500mg", cat: "Antibiotic" },
{ name: "Atorvastatin 20mg", cat: "Statin" },
{ name: "Lisinopril 10mg", cat: "ACE inhibitor" },
{ name: "Metformin 500mg", cat: "Antidiabetic" },
{ name: "Omeprazole 20mg", cat: "PPI" },
{ name: "Ibuprofen 400mg", cat: "NSAID" },
{ name: "Amlodipine 5mg", cat: "Calcium blocker" },
{ name: "Sertraline 50mg", cat: "SSRI" },
{ name: "Levothyroxine 50mcg", cat: "Thyroid" },
{ name: "Salbutamol inhaler", cat: "Bronchodilator" },
];
// ── Element refs ──────────────────────────────────────────────────────────────
const $ = (id) => document.getElementById(id);
const form = $("rxForm");
const drugInput = $("drug");
const acList = $("acList");
const fields = {
dose: $("dose"),
route: $("route"),
frequency: $("frequency"),
duration: $("duration"),
quantity: $("quantity"),
refills: $("refills"),
prn: $("prn"),
notes: $("notes"),
};
const pv = {
drug: $("pvDrug"),
sig: $("pvSig"),
notes: $("pvNotes"),
qty: $("pvQty"),
refills: $("pvRefills"),
};
const rxList = $("rxList");
const emptyState = $("emptyState");
const countPill = $("countPill");
const sendBtn = $("sendBtn");
const toast = $("toast");
let prescriptions = [];
let activeIndex = -1;
// ── Toast ─────────────────────────────────────────────────────────────────────
function showToast(msg, isError) {
toast.textContent = msg;
toast.classList.toggle("is-error", Boolean(isError));
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2800);
}
// ── Sig assembly ──────────────────────────────────────────────────────────────
function buildSig() {
const parts = [];
if (fields.dose.value.trim()) parts.push(fields.dose.value.trim());
if (fields.route.value) parts.push(fields.route.value.toLowerCase());
if (fields.frequency.value) parts.push(fields.frequency.value.toLowerCase());
if (fields.prn.checked) parts.push("as needed (PRN)");
if (fields.duration.value.trim()) parts.push(`for ${fields.duration.value.trim()}`);
return parts.length
? `Sig: ${parts.join(" · ")}`
: "Sig: complete the fields to assemble instructions.";
}
function renderPreview() {
pv.drug.textContent = drugInput.value.trim() || "—";
pv.sig.textContent = buildSig();
pv.qty.textContent = fields.quantity.value || "0";
pv.refills.textContent = fields.refills.value || "0";
const note = fields.notes.value.trim();
pv.notes.textContent = note;
pv.notes.hidden = !note;
}
// ── Autocomplete ──────────────────────────────────────────────────────────────
function highlight(name, q) {
const i = name.toLowerCase().indexOf(q.toLowerCase());
if (i < 0) return name;
return `${name.slice(0, i)}<mark>${name.slice(i, i + q.length)}</mark>${name.slice(i + q.length)}`;
}
function closeList() {
acList.hidden = true;
acList.innerHTML = "";
activeIndex = -1;
drugInput.setAttribute("aria-expanded", "false");
}
function openList() {
const q = drugInput.value.trim();
if (!q) return closeList();
const matches = DRUGS.filter((d) => d.name.toLowerCase().includes(q.toLowerCase()));
activeIndex = -1;
acList.innerHTML = matches.length
? matches
.map(
(d) =>
`<li class="ac-item" role="option" data-name="${d.name}"><span>${highlight(d.name, q)}</span><span class="ac-cat">${d.cat}</span></li>`
)
.join("")
: `<li class="ac-empty">No matching medication</li>`;
acList.hidden = false;
drugInput.setAttribute("aria-expanded", "true");
}
function selectDrug(name) {
drugInput.value = name;
closeList();
renderPreview();
fields.dose.focus();
}
drugInput.addEventListener("input", () => {
openList();
renderPreview();
});
drugInput.addEventListener("focus", openList);
drugInput.addEventListener("keydown", (e) => {
const items = [...acList.querySelectorAll(".ac-item")];
if (!items.length) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
activeIndex += e.key === "ArrowDown" ? 1 : -1;
if (activeIndex < 0) activeIndex = items.length - 1;
if (activeIndex >= items.length) activeIndex = 0;
items.forEach((it, i) => it.classList.toggle("is-active", i === activeIndex));
items[activeIndex].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && activeIndex > -1) {
e.preventDefault();
selectDrug(items[activeIndex].dataset.name);
} else if (e.key === "Escape") {
closeList();
}
});
acList.addEventListener("mousedown", (e) => {
const item = e.target.closest(".ac-item");
if (item) {
e.preventDefault();
selectDrug(item.dataset.name);
}
});
document.addEventListener("click", (e) => {
if (!e.target.closest(".combo")) closeList();
});
// ── Live preview on any field change ──────────────────────────────────────────
form.addEventListener("input", renderPreview);
form.addEventListener("change", renderPreview);
// ── List rendering ────────────────────────────────────────────────────────────
function refreshList() {
countPill.textContent = `${prescriptions.length} item${prescriptions.length === 1 ? "" : "s"}`;
sendBtn.disabled = prescriptions.length === 0;
emptyState.hidden = prescriptions.length > 0;
rxList.querySelectorAll(".rx-item").forEach((n) => n.remove());
prescriptions.forEach((rx, i) => {
const li = document.createElement("li");
li.className = "rx-item";
const sig = rx.sig.replace(/^Sig:\s*/, "");
li.innerHTML =
`<div><p class="li-name">${rx.drug}</p><p class="li-sig">${sig}</p></div>` +
`<button class="remove-btn" data-i="${i}" aria-label="Remove ${rx.drug}">✕</button>`;
rxList.appendChild(li);
});
}
rxList.addEventListener("click", (e) => {
const btn = e.target.closest(".remove-btn");
if (!btn) return;
const removed = prescriptions.splice(Number(btn.dataset.i), 1)[0];
refreshList();
showToast(`Removed ${removed.drug} from the list.`);
});
// ── Add / submit ──────────────────────────────────────────────────────────────
form.addEventListener("submit", (e) => {
e.preventDefault();
drugInput.classList.remove("is-invalid");
fields.frequency.classList.remove("is-invalid");
if (!drugInput.value.trim()) {
drugInput.classList.add("is-invalid");
drugInput.focus();
return showToast("Select or enter a medication first.", true);
}
if (!fields.frequency.value) {
fields.frequency.classList.add("is-invalid");
fields.frequency.focus();
return showToast("Choose a frequency before adding.", true);
}
prescriptions.push({ drug: drugInput.value.trim(), sig: buildSig() });
refreshList();
showToast(`Added ${drugInput.value.trim()} to the prescription.`);
form.reset();
renderPreview();
});
form.addEventListener("reset", () => {
drugInput.classList.remove("is-invalid");
fields.frequency.classList.remove("is-invalid");
setTimeout(renderPreview, 0);
});
// ── Send all ──────────────────────────────────────────────────────────────────
sendBtn.addEventListener("click", () => {
if (!prescriptions.length) return;
const n = prescriptions.length;
prescriptions = [];
refreshList();
showToast(`Sent ${n} prescription${n === 1 ? "" : "s"} to the pharmacy.`);
});
renderPreview();
refreshList();<!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>E-Prescription Pad · 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>
<div class="patient-chip">
<span class="pc-avatar" aria-hidden="true">AM</span>
<span class="pc-info">
<strong>Amara Mensah</strong>
<small>DOB 14 Mar 1986 · MRN 48-2291</small>
</span>
</div>
</header>
<main class="pad">
<section class="pane form-pane">
<header class="pane-head">
<h1>New prescription</h1>
<p class="sub">Search a medication, set the sig, then add it to the list.</p>
</header>
<form id="rxForm" class="rx-form" autocomplete="off" novalidate>
<div class="field combo">
<label for="drug">Medication</label>
<input
id="drug"
name="drug"
type="text"
placeholder="Start typing a drug name…"
role="combobox"
aria-expanded="false"
aria-controls="acList"
aria-autocomplete="list"
/>
<ul id="acList" class="ac-list" role="listbox" aria-label="Medication matches" hidden></ul>
</div>
<div class="row">
<div class="field">
<label for="dose">Dose</label>
<input id="dose" name="dose" type="text" placeholder="e.g. 1 tablet" />
</div>
<div class="field">
<label for="route">Route</label>
<select id="route" name="route">
<option value="Oral">Oral</option>
<option value="Topical">Topical</option>
<option value="Inhaled">Inhaled</option>
<option value="Injection">Injection</option>
</select>
</div>
</div>
<div class="row">
<div class="field">
<label for="frequency">Frequency</label>
<select id="frequency" name="frequency">
<option value="">Select frequency…</option>
<option value="Once daily">Once daily</option>
<option value="Twice daily">Twice daily</option>
<option value="Three times daily">Three times daily</option>
<option value="Every 6 hours">Every 6 hours</option>
<option value="As needed">As needed</option>
</select>
</div>
<div class="field">
<label for="duration">Duration</label>
<input id="duration" name="duration" type="text" placeholder="e.g. 7 days" />
</div>
</div>
<div class="row">
<div class="field">
<label for="quantity">Quantity</label>
<input id="quantity" name="quantity" type="number" min="0" value="30" />
</div>
<div class="field">
<label for="refills">Refills</label>
<input id="refills" name="refills" type="number" min="0" value="0" />
</div>
</div>
<label class="check">
<input id="prn" name="prn" type="checkbox" />
<span>PRN / as needed</span>
</label>
<div class="field">
<label for="notes">Notes / sig instructions</label>
<textarea
id="notes"
name="notes"
rows="2"
placeholder="e.g. Take with food. Stop if rash develops."
></textarea>
</div>
<div class="form-actions">
<button type="reset" class="btn ghost">Clear</button>
<button type="submit" class="btn primary">Add to prescription</button>
</div>
</form>
</section>
<section class="pane preview-pane">
<article class="rx-card" aria-label="Prescription preview">
<header class="rx-top">
<div>
<p class="rx-clinic">Northbridge Health Clinic</p>
<p class="rx-clinic-meta">112 Harbour Road · (555) 014-9920</p>
</div>
<span class="rx-sym" aria-hidden="true">℞</span>
</header>
<div class="rx-patient">
<span>Amara Mensah</span>
<span>DOB 14 Mar 1986</span>
</div>
<div class="rx-body">
<p class="rx-drug" id="pvDrug">—</p>
<p class="rx-sig" id="pvSig">Sig: complete the fields to assemble instructions.</p>
<p class="rx-notes" id="pvNotes" hidden></p>
<div class="rx-meta">
<span>Qty: <strong id="pvQty">30</strong></span>
<span>Refills: <strong id="pvRefills">0</strong></span>
</div>
</div>
<footer class="rx-foot">
<div class="rx-sign">
<span class="rx-signline">Dr. Ravi Patel, MD</span>
<small>Primary Care · DEA AP1234563</small>
</div>
</footer>
</article>
<div class="list-head">
<h2 class="section-h">Prescription list</h2>
<span class="count-pill" id="countPill">0 items</span>
</div>
<ul class="rx-list" id="rxList">
<li class="empty" id="emptyState">No medications added yet.</li>
</ul>
<button class="btn send" id="sendBtn" disabled>Send all to pharmacy</button>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>E-Prescription Pad
The clinician’s side of e-prescribing in a calm clinical-white layout of teal and soft coral. A type-ahead drug search filters a medication list as you type and fills the drug field on select; dose, route, frequency, duration, quantity and refills fields drive a live ℞ Rx preview that assembles the sig line in place, formatted like a real prescription down to the prescriber and clinic footer. Add each Rx to a running prescription list, remove items with one tap, and send the whole list to pharmacy — all validated, accessible, and vanilla JS.
Illustrative UI only — not intended for real medical use.