CookbookLabs and workshopsNo. 24
A chemistry lab manualexperiments, hazards and a titration curve to play with
See it in know.sh
An acid–base titration
Pick the acid, its concentration and volume, and the sodium hydroxide; then add titrant a drop or a few millilitres at a time. The pH is solved exactly from the charge balance, and the note beside it shows the by-hand route for the region you are in. It assumes 25 °C (Kw = 1.0 × 10⁻¹⁴), ideal dilute solutions, one proton per acid and no carbon dioxide from the air; indicator ranges are the usual data-book figures, and an indicator is suggested when its mid-colour falls within 0.10 mL of equivalence.
<div class="tit">
<div class="setup">
<label class="wide">Acid in the flask
<select id="acid">
<option value="hcl">Hydrochloric acid, strong</option>
<option value="ethanoic" selected>Ethanoic (acetic) acid, weak, pKa 4.76</option>
<option value="methanoic">Methanoic (formic) acid, weak, pKa 3.75</option>
</select>
</label>
<label>Acid concentration
<select id="ca"><option value="0.05">0.050 mol/L</option><option value="0.1" selected>0.100 mol/L</option><option value="0.2">0.200 mol/L</option></select>
</label>
<label>Acid volume, by pipette
<select id="va"><option value="10">10.00 mL</option><option value="20">20.00 mL</option><option value="25" selected>25.00 mL</option></select>
</label>
<label>NaOH in the burette
<select id="cb"><option value="0.05">0.050 mol/L</option><option value="0.1" selected>0.100 mol/L</option><option value="0.2">0.200 mol/L</option></select>
</label>
</div>
<svg id="plot" role="img" aria-label="Titration curve"></svg>
<div class="burette">
<label class="slide"><span>NaOH added <b id="vbOut">0.00 mL</b></span>
<input id="vb" type="range" min="0" max="50" step="0.05" value="0" aria-describedby="say">
</label>
<div class="adds" role="group" aria-label="Add sodium hydroxide from the burette">
<button type="button" data-add="0.05">One drop</button>
<button type="button" data-add="0.5">+0.5 mL</button>
<button type="button" data-add="1">+1 mL</button>
<button type="button" data-add="5">+5 mL</button>
<label class="check"><input type="checkbox" id="ahead" checked> Show the curve to come</label>
<button type="button" id="again">Start again</button>
</div>
</div>
<dl class="figures">
<div><dt>pH now</dt><dd id="pOut">–</dd></div>
<div><dt>Equivalence point</dt><dd id="eqOut">–</dd></div>
<div><dt>Half-equivalence</dt><dd id="halfOut">–</dd></div>
<div><dt>Indicator</dt><dd id="indOut">–</dd></div>
</dl>
<p class="say" id="say" aria-live="polite"></p>
<p class="ind" id="ind" aria-live="polite"></p>
</div>.tit { font-family: var(--sans); }
.setup { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px 16px; }
.setup .wide { grid-column: 1 / -1; }
.setup label, .slide { display: grid; gap: 3px; }
.setup select { width: 100%; min-height: 36px; }
#plot { display: block; width: 100%; margin-top: 12px; overflow: visible; }
.axis { stroke: var(--ink); stroke-width: 1; fill: none; }
.grid { stroke: var(--rule); stroke-width: 1; }
.tick { font: 400 11px var(--sans); fill: var(--ink-3); font-variant-numeric: tabular-nums; }
.atitle { font: 400 11.5px var(--sans); fill: var(--ink-2); }
.band { fill: var(--wash); }
.bandlab { font: italic 400 11.5px var(--serif); fill: var(--ink-2); }
.ahead { fill: none; stroke: var(--ink-3); stroke-width: 1.2; stroke-dasharray: 3 3; }
.done { fill: none; stroke: var(--ink); stroke-width: 2.2; stroke-linejoin: round; }
.mark { fill: var(--paper); stroke: var(--ink); stroke-width: 1.5; }
.now { fill: var(--ink); }
.guide { stroke: var(--ink-2); stroke-width: 1; stroke-dasharray: 1 3; }
.marklab { font: 500 11.5px var(--sans); fill: var(--ink); }
.halo { stroke: var(--paper); stroke-width: 4px; stroke-linejoin: round; paint-order: stroke; }
.burette { margin-top: 6px; }
.slide b { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.slide input { width: 100%; }
.adds { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; }
.adds button { font: 500 13.5px/1 var(--sans); color: var(--ink); background: var(--paper); border: 1px solid var(--ink); padding: 9px 11px; min-height: 38px; }
.adds button:active { background: var(--ink); color: var(--paper); }
.adds #again { margin-left: auto; border-color: var(--rule); color: var(--ink-2); }
.check { display: flex; align-items: center; gap: 6px; margin-left: 6px; }
.figures { display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 0 18px; margin: 12px 0 0; border-top: 1px solid var(--ink); }
.figures div { padding: 6px 0; border-bottom: 1px solid var(--rule); }
.figures dt { font: 400 12.5px/1.3 var(--sans); color: var(--ink-3); }
.figures dd { margin: 2px 0 0; font: 400 16px/1.3 var(--serif); color: var(--ink); font-variant-numeric: tabular-nums; }
.say, .ind { margin: 8px 0 0; font: 400 15px/1.45 var(--serif); color: var(--ink); }
.ind { color: var(--ink-2); }
.say i, .ind i { font-style: italic; }
button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
@media (max-width: 420px) { .adds #again { margin-left: 0; } }(function () {
var $ = function (id) { return document.getElementById(id); };
var svg = $('plot');
var KW = 1e-14, DROP = 0.05, NEAR = 0.1, BURETTE = 50;
var ACIDS = {
hcl: { name: 'hydrochloric acid', strong: true, salt: 'sodium chloride' },
ethanoic: { name: 'ethanoic acid', pKa: 4.76, ion: 'ethanoate', salt: 'sodium ethanoate' },
methanoic: { name: 'methanoic acid', pKa: 3.75, ion: 'methanoate', salt: 'sodium methanoate' }
};
// Transition ranges as school data books give them, with the colour on each side.
var INDICATORS = [
{ name: 'Methyl orange', lo: 3.1, hi: 4.4, acid: 'red', base: 'yellow' },
{ name: 'Methyl red', lo: 4.4, hi: 6.2, acid: 'red', base: 'yellow' },
{ name: 'Bromothymol blue', lo: 6.0, hi: 7.6, acid: 'yellow', base: 'blue' },
{ name: 'Phenolphthalein', lo: 8.2, hi: 10.0, acid: 'colourless', base: 'pink' },
{ name: 'Thymolphthalein', lo: 9.3, hi: 10.5, acid: 'colourless', base: 'blue' }
];
var s = { acid: 'ethanoic', ca: 0.1, va: 25, cb: 0.1, vb: 0, ahead: true };
var cache = null;
function acid() { return ACIDS[s.acid]; }
function ka() { var a = acid(); return a.strong ? Infinity : Math.pow(10, -a.pKa); }
function veq() { return s.ca * s.va / s.cb; }
function f2(x) { return (Math.round(x * 100) / 100).toFixed(2); }
function mmol(x) { return x < 0.1 ? x.toFixed(3) : f2(x); }
// The exact pH, ignoring activities: solve the charge balance
// [H+] + [Na+] = [OH-] + [A-], with [A-] = C·Ka/(Ka + [H+]) for a weak acid
// and [A-] = C for a strong one. The left side minus the right rises with
// [H+], so bisection on pH always finds the one root.
function pHAt(vb) {
var vt = s.va + vb, c = s.ca * s.va / vt, na = s.cb * vb / vt, k = ka(), lo = -2, hi = 16;
for (var i = 0; i < 70; i++) {
var mid = (lo + hi) / 2, h = Math.pow(10, -mid);
var a = k === Infinity ? c : c * k / (k + h);
if (h + na - KW / h - a > 0) lo = mid; else hi = mid;
}
return (lo + hi) / 2;
}
function sci(x) {
if (x >= 0.001) return x.toPrecision(3);
var e = Math.floor(Math.log10(x)), m = x / Math.pow(10, e);
if (m >= 9.995) { m /= 10; e += 1; }
var sup = { '-': '⁻', '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹' };
return m.toFixed(2) + ' × 10' + String(e).split('').map(function (ch) { return sup[ch]; }).join('');
}
// The route a student takes by hand in each region, and its answer.
function byHand(vb) {
var a = acid(), nA = s.ca * s.va, nB = s.cb * vb, vt = s.va + vb, rel = (nB - nA) / nA, excess, conc, p;
var tot = 'total volume ' + f2(vt) + ' mL';
if (Math.abs(rel) < 1e-9) {
if (a.strong) return { p: 7, text: '<b>At equivalence.</b> The flask holds only ' + a.salt + ' and water, a neutral salt, so pH = 7.00.' };
var c = nA / vt, kb = KW / ka(), oh = Math.sqrt(kb * c);
p = 14 + Math.log10(oh);
return { p: p, text: '<b>At equivalence.</b> Every acid molecule has become ' + a.ion + ', a weak base that takes H⁺ from water. By hand: K<sub>b</sub> = K<sub>w</sub> ÷ K<sub>a</sub> = ' + sci(kb) + ', c = ' + mmol(nA) + ' mmol ÷ ' + f2(vt) + ' mL = ' + sci(c) + ' mol/L, [OH⁻] = √(K<sub>b</sub>·c) = ' + sci(oh) + ', so pH = 14 − pOH = ' + f2(p) + '.' };
}
if (nB > nA) {
excess = nB - nA; conc = excess / vt; p = 14 + Math.log10(conc);
return { p: p, text: '<b>After equivalence.</b> Excess sodium hydroxide sets the pH' + (a.strong ? '' : ' and swamps the ' + a.ion) + '. By hand: [OH⁻] = ' + mmol(excess) + ' mmol ÷ ' + f2(vt) + ' mL = ' + sci(conc) + ' mol/L, so pH = 14 − pOH = ' + f2(p) + '.' };
}
if (a.strong) {
excess = nA - nB; conc = excess / vt; p = -Math.log10(conc);
return { p: p, text: (vb === 0 ? '<b>Acid alone.</b> Hydrochloric acid ionises completely' : '<b>Before equivalence.</b> Hydroxide has neutralised part of the acid; the rest ionises completely') + '. By hand: [H⁺] = ' + mmol(excess) + ' mmol ÷ ' + f2(vt) + ' mL = ' + sci(conc) + ' mol/L, so pH = ' + f2(p) + '.' };
}
if (vb === 0) {
p = 0.5 * (a.pKa - Math.log10(s.ca));
return { p: p, text: '<b>Acid alone.</b> A weak acid ionises only a little. By hand: [H⁺] ≈ √(K<sub>a</sub>·c), so pH ≈ ½(pK<sub>a</sub> − log c) = ½(' + a.pKa.toFixed(2) + ' − log ' + s.ca.toFixed(3) + ') = ' + f2(p) + '.' };
}
p = a.pKa + Math.log10(nB / (nA - nB));
return { p: p, text: '<b>Buffer region.</b> The flask holds ' + a.name + ' and ' + a.ion + ' together. By hand (Henderson–Hasselbalch): pH = pK<sub>a</sub> + log(' + a.ion + ' ÷ acid) = ' + a.pKa.toFixed(2) + ' + log(' + mmol(nB) + ' mmol ÷ ' + mmol(nA - nB) + ' mmol) = ' + f2(p) + '.' };
}
function setup() {
var v = veq(), a = acid(), xmax = Math.max(10, Math.ceil(2 * v / 5) * 5), xs = [], i, d;
for (i = 0; i <= 320; i++) xs.push(xmax * i / 320);
[1e-5, 3e-5, 1e-4, 3e-4, 1e-3, 2e-3, 5e-3, 1e-2, 2e-2, 4e-2, 7e-2].forEach(function (d) { xs.push(v * (1 - d)); xs.push(v * (1 + d)); xs.push(v * d); });
xs.push(v);
xs = xs.filter(function (x) { return x >= 0 && x <= xmax; }).sort(function (p, q) { return p - q; });
var pts = xs.map(function (x) { return [x, pHAt(x)]; });
// An indicator suits the titration when its mid-colour is reached within
// 0.10 mL (two drops) either side of equivalence.
var lo = pHAt(Math.max(0, v - NEAR)), hi = pHAt(v + NEAR), pe = pHAt(v);
var mid = function (n) { return (n.lo + n.hi) / 2; };
var fits = INDICATORS.filter(function (n) { return mid(n) >= lo && mid(n) <= hi; });
var closest = function (list) { return list.slice().sort(function (p, q) { return Math.abs((p.lo + p.hi) / 2 - pe) - Math.abs((q.lo + q.hi) / 2 - pe); })[0]; };
var pick = fits.length ? closest(fits) : closest(INDICATORS);
cache = { v: v, xmax: xmax, pts: pts, jump: [lo, hi], pe: pe, fits: fits, pick: pick, half: a.strong ? null : pHAt(v / 2) };
var slider = $('vb'); slider.max = xmax;
if (s.vb > xmax) s.vb = xmax;
}
function draw() {
var c = cache, a = acid();
var W = Math.max(260, Math.round(svg.getBoundingClientRect().width) || 588), H = W < 440 ? 230 : 252;
var L = 30, R = 12, T = 12, B = 34, pw = W - L - R, ph = H - T - B;
var X = function (v) { return L + v / c.xmax * pw; };
var Y = function (p) { return T + (14 - Math.max(0, Math.min(14, p))) / 14 * ph; };
var out = [], i;
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H); svg.setAttribute('height', H);
var n = c.pick;
out.push('<rect class="band" x="' + L + '" y="' + Y(n.hi) + '" width="' + pw + '" height="' + (Y(n.lo) - Y(n.hi)) + '"/>');
for (i = 0; i <= 14; i += 2) {
out.push('<line class="grid" x1="' + L + '" x2="' + (L + pw) + '" y1="' + Y(i) + '" y2="' + Y(i) + '"/>');
out.push('<text class="tick" x="' + (L - 6) + '" y="' + (Y(i) + 4) + '" text-anchor="end">' + i + '</text>');
}
var steps = [1, 2, 5, 10, 20, 25, 50], step = steps.filter(function (st) { return c.xmax / st <= (W < 440 ? 5 : 8); })[0] || 50;
for (var x = 0; x <= c.xmax + 1e-9; x += step) {
out.push('<line class="axis" x1="' + X(x) + '" x2="' + X(x) + '" y1="' + (T + ph) + '" y2="' + (T + ph + 4) + '"/>');
out.push('<text class="tick" x="' + X(x) + '" y="' + (T + ph + 16) + '" text-anchor="middle">' + x + '</text>');
}
out.push('<path class="axis" d="M' + L + ' ' + T + 'V' + (T + ph) + 'H' + (L + pw) + '"/>');
out.push('<text class="atitle" x="' + (L + pw) + '" y="' + (H - 2) + '" text-anchor="end">NaOH added, mL</text>');
out.push('<text class="atitle" x="' + (L + 4) + '" y="' + (T + 10) + '">pH</text>');
out.push('<text class="bandlab halo" x="' + (L + 6) + '" y="' + (Y(n.hi) - 4) + '">' + n.name.toLowerCase() + ' changes colour</text>');
var line = function (pts) { return pts.map(function (p, j) { return (j ? 'L' : 'M') + X(p[0]).toFixed(1) + ' ' + Y(p[1]).toFixed(1); }).join(''); };
var pNow = pHAt(s.vb);
var done = c.pts.filter(function (p) { return p[0] < s.vb; }).concat([[s.vb, pNow]]);
var rest = [[s.vb, pNow]].concat(c.pts.filter(function (p) { return p[0] > s.vb; }));
if (s.ahead && rest.length > 1) out.push('<path class="ahead" d="' + line(rest) + '"/>');
if (done.length > 1) out.push('<path class="done" d="' + line(done) + '"/>');
var ex = X(c.v), ey = Y(c.pe), right = ex < L + pw * 0.62;
out.push('<line class="guide" x1="' + ex + '" x2="' + ex + '" y1="' + T + '" y2="' + (T + ph) + '"/>');
out.push('<circle class="mark" cx="' + ex + '" cy="' + ey + '" r="4.5"/>');
out.push('<text class="marklab halo" x="' + (ex + (right ? 7 : -7)) + '" y="' + (T + 12) + '" text-anchor="' + (right ? 'start' : 'end') + '">Equivalence</text>');
if (c.half !== null) {
var hx = X(c.v / 2), hy = Y(c.half);
out.push('<line class="guide" x1="' + L + '" x2="' + hx + '" y1="' + hy + '" y2="' + hy + '"/>');
out.push('<circle class="mark" cx="' + hx + '" cy="' + hy + '" r="4.5"/>');
out.push('<text class="marklab halo" x="' + (hx + 7) + '" y="' + (hy + 16) + '">Half-equivalence: pH = pKa</text>');
}
out.push('<circle class="now" cx="' + X(s.vb) + '" cy="' + Y(pNow) + '" r="5"/>');
svg.innerHTML = out.join('');
svg.setAttribute('aria-label', 'Titration curve of pH against NaOH added, 0 to ' + c.xmax + ' mL, for ' + s.ca.toFixed(3) + ' mol/L ' + a.name + '. It rises steeply at the equivalence point, ' + f2(c.v) + ' mL, pH ' + f2(c.pe) + '. Now at ' + f2(s.vb) + ' mL, pH ' + f2(pNow) + '.');
return pNow;
}
function colourIn(n, p) {
if (p < n.lo) return n.acid;
if (p > n.hi) return n.base;
return 'changing, between ' + n.acid + ' and ' + n.base;
}
function render() {
var c = cache, a = acid(), p = draw(), hand = byHand(s.vb), n = c.pick;
$('vb').value = s.vb;
$('vbOut').textContent = f2(s.vb) + ' mL';
$('pOut').textContent = f2(p);
$('eqOut').textContent = f2(c.v) + ' mL, pH ' + f2(c.pe);
$('halfOut').textContent = c.half === null ? 'None: strong acid' : f2(c.v / 2) + ' mL, pH ' + f2(c.half);
$('indOut').textContent = n.name + ', ' + n.lo.toFixed(1) + '–' + n.hi.toFixed(1);
var say = hand.text;
if (Math.abs(hand.p - p) >= 0.05) say += ' <i>The shortcut is off by ' + Math.abs(hand.p - p).toFixed(2) + ' here: so close to the start or to equivalence it leaves out the acid’s own ionisation or water’s. The exact pH, ' + f2(p) + ', allows for both.</i>';
if (c.v > BURETTE) say += ' <i>Equivalence needs more than one 50 mL burette: take a smaller aliquot of acid or a stronger NaOH.</i>';
$('say').innerHTML = say;
var others = c.fits.filter(function (m) { return m !== n; }).map(function (m) { return m.name.toLowerCase(); });
var why = c.fits.length
? n.name + ' suits it best: its mid-colour, pH ' + ((n.lo + n.hi) / 2).toFixed(1) + ', is nearest the equivalence pH and falls inside the jump, where 0.10 mL (two drops) either side of equivalence takes the pH from ' + f2(c.jump[0]) + ' to ' + f2(c.jump[1]) + '.' + (others.length ? ' Also inside it: ' + others.join(', ') + '.' : '')
: 'No common indicator reaches its mid-colour within 0.10 mL of equivalence (pH ' + f2(c.jump[0]) + ' to ' + f2(c.jump[1]) + '); ' + n.name.toLowerCase() + ' comes closest. Add the last millilitre drop by drop, or use a pH meter.';
$('ind').textContent = why + ' In the flask now: ' + colourIn(n, p) + '.';
}
function set(v) { s.vb = Math.max(0, Math.min(cache.xmax, Math.round(v / DROP) * DROP)); s.vb = Math.round(s.vb * 100) / 100; render(); }
['acid', 'ca', 'va', 'cb'].forEach(function (id) {
$(id).addEventListener('change', function () {
s[id] = id === 'acid' ? this.value : Number(this.value);
s.vb = 0; setup(); render();
});
});
$('vb').addEventListener('input', function () { set(Number(this.value)); });
document.querySelectorAll('[data-add]').forEach(function (b) {
b.addEventListener('click', function () { set(s.vb + Number(b.getAttribute('data-add'))); });
});
$('again').addEventListener('click', function () { set(0); });
$('ahead').addEventListener('change', function () { s.ahead = this.checked; render(); });
document.addEventListener('know-tokens', render);
var lastW = 0;
if (window.ResizeObserver) new ResizeObserver(function (en) { var w = Math.round(en[0].contentRect.width); if (w !== lastW) { lastW = w; requestAnimationFrame(render); } }).observe(svg.parentNode);
else window.addEventListener('resize', render);
setup(); render();
})();```element widget
{
"title": "An acid–base titration",
"caption": "Pick the acid, its concentration and volume, and the sodium hydroxide; then add titrant a drop or a few millilitres at a time. The pH is solved exactly from the charge balance, and the note beside it shows the by-hand route for the region you are in. It assumes 25 °C (Kw = 1.0 × 10⁻¹⁴), ideal dilute solutions, one proton per acid and no carbon dioxide from the air; indicator ranges are the usual data-book figures, and an indicator is suggested when its mid-colour falls within 0.10 mL of equivalence.",
"height": 730,
"html": "<div class=\"tit\">\n <div class=\"setup\">\n <label class=\"wide\">Acid in the flask\n <select id=\"acid\">\n <option value=\"hcl\">Hydrochloric acid, strong</option>\n <option value=\"ethanoic\" selected>Ethanoic (acetic) acid, weak, pKa 4.76</option>\n <option value=\"methanoic\">Methanoic (formic) acid, weak, pKa 3.75</option>\n </select>\n </label>\n <label>Acid concentration\n <select id=\"ca\"><option value=\"0.05\">0.050 mol/L</option><option value=\"0.1\" selected>0.100 mol/L</option><option value=\"0.2\">0.200 mol/L</option></select>\n </label>\n <label>Acid volume, by pipette\n <select id=\"va\"><option value=\"10\">10.00 mL</option><option value=\"20\">20.00 mL</option><option value=\"25\" selected>25.00 mL</option></select>\n </label>\n <label>NaOH in the burette\n <select id=\"cb\"><option value=\"0.05\">0.050 mol/L</option><option value=\"0.1\" selected>0.100 mol/L</option><option value=\"0.2\">0.200 mol/L</option></select>\n </label>\n </div>\n\n <svg id=\"plot\" role=\"img\" aria-label=\"Titration curve\"></svg>\n\n <div class=\"burette\">\n <label class=\"slide\"><span>NaOH added <b id=\"vbOut\">0.00 mL</b></span>\n <input id=\"vb\" type=\"range\" min=\"0\" max=\"50\" step=\"0.05\" value=\"0\" aria-describedby=\"say\">\n </label>\n <div class=\"adds\" role=\"group\" aria-label=\"Add sodium hydroxide from the burette\">\n <button type=\"button\" data-add=\"0.05\">One drop</button>\n <button type=\"button\" data-add=\"0.5\">+0.5 mL</button>\n <button type=\"button\" data-add=\"1\">+1 mL</button>\n <button type=\"button\" data-add=\"5\">+5 mL</button>\n <label class=\"check\"><input type=\"checkbox\" id=\"ahead\" checked> Show the curve to come</label>\n <button type=\"button\" id=\"again\">Start again</button>\n </div>\n </div>\n\n <dl class=\"figures\">\n <div><dt>pH now</dt><dd id=\"pOut\">–</dd></div>\n <div><dt>Equivalence point</dt><dd id=\"eqOut\">–</dd></div>\n <div><dt>Half-equivalence</dt><dd id=\"halfOut\">–</dd></div>\n <div><dt>Indicator</dt><dd id=\"indOut\">–</dd></div>\n </dl>\n <p class=\"say\" id=\"say\" aria-live=\"polite\"></p>\n <p class=\"ind\" id=\"ind\" aria-live=\"polite\"></p>\n</div>",
"css": ".tit { font-family: var(--sans); }\n.setup { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px 16px; }\n.setup .wide { grid-column: 1 / -1; }\n.setup label, .slide { display: grid; gap: 3px; }\n.setup select { width: 100%; min-height: 36px; }\n#plot { display: block; width: 100%; margin-top: 12px; overflow: visible; }\n.axis { stroke: var(--ink); stroke-width: 1; fill: none; }\n.grid { stroke: var(--rule); stroke-width: 1; }\n.tick { font: 400 11px var(--sans); fill: var(--ink-3); font-variant-numeric: tabular-nums; }\n.atitle { font: 400 11.5px var(--sans); fill: var(--ink-2); }\n.band { fill: var(--wash); }\n.bandlab { font: italic 400 11.5px var(--serif); fill: var(--ink-2); }\n.ahead { fill: none; stroke: var(--ink-3); stroke-width: 1.2; stroke-dasharray: 3 3; }\n.done { fill: none; stroke: var(--ink); stroke-width: 2.2; stroke-linejoin: round; }\n.mark { fill: var(--paper); stroke: var(--ink); stroke-width: 1.5; }\n.now { fill: var(--ink); }\n.guide { stroke: var(--ink-2); stroke-width: 1; stroke-dasharray: 1 3; }\n.marklab { font: 500 11.5px var(--sans); fill: var(--ink); }\n.halo { stroke: var(--paper); stroke-width: 4px; stroke-linejoin: round; paint-order: stroke; }\n.burette { margin-top: 6px; }\n.slide b { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }\n.slide input { width: 100%; }\n.adds { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; }\n.adds button { font: 500 13.5px/1 var(--sans); color: var(--ink); background: var(--paper); border: 1px solid var(--ink); padding: 9px 11px; min-height: 38px; }\n.adds button:active { background: var(--ink); color: var(--paper); }\n.adds #again { margin-left: auto; border-color: var(--rule); color: var(--ink-2); }\n.check { display: flex; align-items: center; gap: 6px; margin-left: 6px; }\n.figures { display: grid; grid-template-columns: repeat(auto-fit, minmax(128px, 1fr)); gap: 0 18px; margin: 12px 0 0; border-top: 1px solid var(--ink); }\n.figures div { padding: 6px 0; border-bottom: 1px solid var(--rule); }\n.figures dt { font: 400 12.5px/1.3 var(--sans); color: var(--ink-3); }\n.figures dd { margin: 2px 0 0; font: 400 16px/1.3 var(--serif); color: var(--ink); font-variant-numeric: tabular-nums; }\n.say, .ind { margin: 8px 0 0; font: 400 15px/1.45 var(--serif); color: var(--ink); }\n.ind { color: var(--ink-2); }\n.say i, .ind i { font-style: italic; }\nbutton:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }\n@media (max-width: 420px) { .adds #again { margin-left: 0; } }",
"js": "(function () {\n var $ = function (id) { return document.getElementById(id); };\n var svg = $('plot');\n var KW = 1e-14, DROP = 0.05, NEAR = 0.1, BURETTE = 50;\n var ACIDS = {\n hcl: { name: 'hydrochloric acid', strong: true, salt: 'sodium chloride' },\n ethanoic: { name: 'ethanoic acid', pKa: 4.76, ion: 'ethanoate', salt: 'sodium ethanoate' },\n methanoic: { name: 'methanoic acid', pKa: 3.75, ion: 'methanoate', salt: 'sodium methanoate' }\n };\n // Transition ranges as school data books give them, with the colour on each side.\n var INDICATORS = [\n { name: 'Methyl orange', lo: 3.1, hi: 4.4, acid: 'red', base: 'yellow' },\n { name: 'Methyl red', lo: 4.4, hi: 6.2, acid: 'red', base: 'yellow' },\n { name: 'Bromothymol blue', lo: 6.0, hi: 7.6, acid: 'yellow', base: 'blue' },\n { name: 'Phenolphthalein', lo: 8.2, hi: 10.0, acid: 'colourless', base: 'pink' },\n { name: 'Thymolphthalein', lo: 9.3, hi: 10.5, acid: 'colourless', base: 'blue' }\n ];\n var s = { acid: 'ethanoic', ca: 0.1, va: 25, cb: 0.1, vb: 0, ahead: true };\n var cache = null;\n\n function acid() { return ACIDS[s.acid]; }\n function ka() { var a = acid(); return a.strong ? Infinity : Math.pow(10, -a.pKa); }\n function veq() { return s.ca * s.va / s.cb; }\n function f2(x) { return (Math.round(x * 100) / 100).toFixed(2); }\n function mmol(x) { return x < 0.1 ? x.toFixed(3) : f2(x); }\n\n // The exact pH, ignoring activities: solve the charge balance\n // [H+] + [Na+] = [OH-] + [A-], with [A-] = C·Ka/(Ka + [H+]) for a weak acid\n // and [A-] = C for a strong one. The left side minus the right rises with\n // [H+], so bisection on pH always finds the one root.\n function pHAt(vb) {\n var vt = s.va + vb, c = s.ca * s.va / vt, na = s.cb * vb / vt, k = ka(), lo = -2, hi = 16;\n for (var i = 0; i < 70; i++) {\n var mid = (lo + hi) / 2, h = Math.pow(10, -mid);\n var a = k === Infinity ? c : c * k / (k + h);\n if (h + na - KW / h - a > 0) lo = mid; else hi = mid;\n }\n return (lo + hi) / 2;\n }\n\n function sci(x) {\n if (x >= 0.001) return x.toPrecision(3);\n var e = Math.floor(Math.log10(x)), m = x / Math.pow(10, e);\n if (m >= 9.995) { m /= 10; e += 1; }\n var sup = { '-': '⁻', '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹' };\n return m.toFixed(2) + ' × 10' + String(e).split('').map(function (ch) { return sup[ch]; }).join('');\n }\n\n // The route a student takes by hand in each region, and its answer.\n function byHand(vb) {\n var a = acid(), nA = s.ca * s.va, nB = s.cb * vb, vt = s.va + vb, rel = (nB - nA) / nA, excess, conc, p;\n var tot = 'total volume ' + f2(vt) + ' mL';\n if (Math.abs(rel) < 1e-9) {\n if (a.strong) return { p: 7, text: '<b>At equivalence.</b> The flask holds only ' + a.salt + ' and water, a neutral salt, so pH = 7.00.' };\n var c = nA / vt, kb = KW / ka(), oh = Math.sqrt(kb * c);\n p = 14 + Math.log10(oh);\n return { p: p, text: '<b>At equivalence.</b> Every acid molecule has become ' + a.ion + ', a weak base that takes H⁺ from water. By hand: K<sub>b</sub> = K<sub>w</sub> ÷ K<sub>a</sub> = ' + sci(kb) + ', c = ' + mmol(nA) + ' mmol ÷ ' + f2(vt) + ' mL = ' + sci(c) + ' mol/L, [OH⁻] = √(K<sub>b</sub>·c) = ' + sci(oh) + ', so pH = 14 − pOH = ' + f2(p) + '.' };\n }\n if (nB > nA) {\n excess = nB - nA; conc = excess / vt; p = 14 + Math.log10(conc);\n return { p: p, text: '<b>After equivalence.</b> Excess sodium hydroxide sets the pH' + (a.strong ? '' : ' and swamps the ' + a.ion) + '. By hand: [OH⁻] = ' + mmol(excess) + ' mmol ÷ ' + f2(vt) + ' mL = ' + sci(conc) + ' mol/L, so pH = 14 − pOH = ' + f2(p) + '.' };\n }\n if (a.strong) {\n excess = nA - nB; conc = excess / vt; p = -Math.log10(conc);\n return { p: p, text: (vb === 0 ? '<b>Acid alone.</b> Hydrochloric acid ionises completely' : '<b>Before equivalence.</b> Hydroxide has neutralised part of the acid; the rest ionises completely') + '. By hand: [H⁺] = ' + mmol(excess) + ' mmol ÷ ' + f2(vt) + ' mL = ' + sci(conc) + ' mol/L, so pH = ' + f2(p) + '.' };\n }\n if (vb === 0) {\n p = 0.5 * (a.pKa - Math.log10(s.ca));\n return { p: p, text: '<b>Acid alone.</b> A weak acid ionises only a little. By hand: [H⁺] ≈ √(K<sub>a</sub>·c), so pH ≈ ½(pK<sub>a</sub> − log c) = ½(' + a.pKa.toFixed(2) + ' − log ' + s.ca.toFixed(3) + ') = ' + f2(p) + '.' };\n }\n p = a.pKa + Math.log10(nB / (nA - nB));\n return { p: p, text: '<b>Buffer region.</b> The flask holds ' + a.name + ' and ' + a.ion + ' together. By hand (Henderson–Hasselbalch): pH = pK<sub>a</sub> + log(' + a.ion + ' ÷ acid) = ' + a.pKa.toFixed(2) + ' + log(' + mmol(nB) + ' mmol ÷ ' + mmol(nA - nB) + ' mmol) = ' + f2(p) + '.' };\n }\n\n function setup() {\n var v = veq(), a = acid(), xmax = Math.max(10, Math.ceil(2 * v / 5) * 5), xs = [], i, d;\n for (i = 0; i <= 320; i++) xs.push(xmax * i / 320);\n [1e-5, 3e-5, 1e-4, 3e-4, 1e-3, 2e-3, 5e-3, 1e-2, 2e-2, 4e-2, 7e-2].forEach(function (d) { xs.push(v * (1 - d)); xs.push(v * (1 + d)); xs.push(v * d); });\n xs.push(v);\n xs = xs.filter(function (x) { return x >= 0 && x <= xmax; }).sort(function (p, q) { return p - q; });\n var pts = xs.map(function (x) { return [x, pHAt(x)]; });\n // An indicator suits the titration when its mid-colour is reached within\n // 0.10 mL (two drops) either side of equivalence.\n var lo = pHAt(Math.max(0, v - NEAR)), hi = pHAt(v + NEAR), pe = pHAt(v);\n var mid = function (n) { return (n.lo + n.hi) / 2; };\n var fits = INDICATORS.filter(function (n) { return mid(n) >= lo && mid(n) <= hi; });\n var closest = function (list) { return list.slice().sort(function (p, q) { return Math.abs((p.lo + p.hi) / 2 - pe) - Math.abs((q.lo + q.hi) / 2 - pe); })[0]; };\n var pick = fits.length ? closest(fits) : closest(INDICATORS);\n cache = { v: v, xmax: xmax, pts: pts, jump: [lo, hi], pe: pe, fits: fits, pick: pick, half: a.strong ? null : pHAt(v / 2) };\n var slider = $('vb'); slider.max = xmax;\n if (s.vb > xmax) s.vb = xmax;\n }\n\n function draw() {\n var c = cache, a = acid();\n var W = Math.max(260, Math.round(svg.getBoundingClientRect().width) || 588), H = W < 440 ? 230 : 252;\n var L = 30, R = 12, T = 12, B = 34, pw = W - L - R, ph = H - T - B;\n var X = function (v) { return L + v / c.xmax * pw; };\n var Y = function (p) { return T + (14 - Math.max(0, Math.min(14, p))) / 14 * ph; };\n var out = [], i;\n svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H); svg.setAttribute('height', H);\n\n var n = c.pick;\n out.push('<rect class=\"band\" x=\"' + L + '\" y=\"' + Y(n.hi) + '\" width=\"' + pw + '\" height=\"' + (Y(n.lo) - Y(n.hi)) + '\"/>');\n for (i = 0; i <= 14; i += 2) {\n out.push('<line class=\"grid\" x1=\"' + L + '\" x2=\"' + (L + pw) + '\" y1=\"' + Y(i) + '\" y2=\"' + Y(i) + '\"/>');\n out.push('<text class=\"tick\" x=\"' + (L - 6) + '\" y=\"' + (Y(i) + 4) + '\" text-anchor=\"end\">' + i + '</text>');\n }\n var steps = [1, 2, 5, 10, 20, 25, 50], step = steps.filter(function (st) { return c.xmax / st <= (W < 440 ? 5 : 8); })[0] || 50;\n for (var x = 0; x <= c.xmax + 1e-9; x += step) {\n out.push('<line class=\"axis\" x1=\"' + X(x) + '\" x2=\"' + X(x) + '\" y1=\"' + (T + ph) + '\" y2=\"' + (T + ph + 4) + '\"/>');\n out.push('<text class=\"tick\" x=\"' + X(x) + '\" y=\"' + (T + ph + 16) + '\" text-anchor=\"middle\">' + x + '</text>');\n }\n out.push('<path class=\"axis\" d=\"M' + L + ' ' + T + 'V' + (T + ph) + 'H' + (L + pw) + '\"/>');\n out.push('<text class=\"atitle\" x=\"' + (L + pw) + '\" y=\"' + (H - 2) + '\" text-anchor=\"end\">NaOH added, mL</text>');\n out.push('<text class=\"atitle\" x=\"' + (L + 4) + '\" y=\"' + (T + 10) + '\">pH</text>');\n out.push('<text class=\"bandlab halo\" x=\"' + (L + 6) + '\" y=\"' + (Y(n.hi) - 4) + '\">' + n.name.toLowerCase() + ' changes colour</text>');\n\n var line = function (pts) { return pts.map(function (p, j) { return (j ? 'L' : 'M') + X(p[0]).toFixed(1) + ' ' + Y(p[1]).toFixed(1); }).join(''); };\n var pNow = pHAt(s.vb);\n var done = c.pts.filter(function (p) { return p[0] < s.vb; }).concat([[s.vb, pNow]]);\n var rest = [[s.vb, pNow]].concat(c.pts.filter(function (p) { return p[0] > s.vb; }));\n if (s.ahead && rest.length > 1) out.push('<path class=\"ahead\" d=\"' + line(rest) + '\"/>');\n if (done.length > 1) out.push('<path class=\"done\" d=\"' + line(done) + '\"/>');\n\n var ex = X(c.v), ey = Y(c.pe), right = ex < L + pw * 0.62;\n out.push('<line class=\"guide\" x1=\"' + ex + '\" x2=\"' + ex + '\" y1=\"' + T + '\" y2=\"' + (T + ph) + '\"/>');\n out.push('<circle class=\"mark\" cx=\"' + ex + '\" cy=\"' + ey + '\" r=\"4.5\"/>');\n out.push('<text class=\"marklab halo\" x=\"' + (ex + (right ? 7 : -7)) + '\" y=\"' + (T + 12) + '\" text-anchor=\"' + (right ? 'start' : 'end') + '\">Equivalence</text>');\n if (c.half !== null) {\n var hx = X(c.v / 2), hy = Y(c.half);\n out.push('<line class=\"guide\" x1=\"' + L + '\" x2=\"' + hx + '\" y1=\"' + hy + '\" y2=\"' + hy + '\"/>');\n out.push('<circle class=\"mark\" cx=\"' + hx + '\" cy=\"' + hy + '\" r=\"4.5\"/>');\n out.push('<text class=\"marklab halo\" x=\"' + (hx + 7) + '\" y=\"' + (hy + 16) + '\">Half-equivalence: pH = pKa</text>');\n }\n out.push('<circle class=\"now\" cx=\"' + X(s.vb) + '\" cy=\"' + Y(pNow) + '\" r=\"5\"/>');\n svg.innerHTML = out.join('');\n svg.setAttribute('aria-label', 'Titration curve of pH against NaOH added, 0 to ' + c.xmax + ' mL, for ' + s.ca.toFixed(3) + ' mol/L ' + a.name + '. It rises steeply at the equivalence point, ' + f2(c.v) + ' mL, pH ' + f2(c.pe) + '. Now at ' + f2(s.vb) + ' mL, pH ' + f2(pNow) + '.');\n return pNow;\n }\n\n function colourIn(n, p) {\n if (p < n.lo) return n.acid;\n if (p > n.hi) return n.base;\n return 'changing, between ' + n.acid + ' and ' + n.base;\n }\n\n function render() {\n var c = cache, a = acid(), p = draw(), hand = byHand(s.vb), n = c.pick;\n $('vb').value = s.vb;\n $('vbOut').textContent = f2(s.vb) + ' mL';\n $('pOut').textContent = f2(p);\n $('eqOut').textContent = f2(c.v) + ' mL, pH ' + f2(c.pe);\n $('halfOut').textContent = c.half === null ? 'None: strong acid' : f2(c.v / 2) + ' mL, pH ' + f2(c.half);\n $('indOut').textContent = n.name + ', ' + n.lo.toFixed(1) + '–' + n.hi.toFixed(1);\n var say = hand.text;\n if (Math.abs(hand.p - p) >= 0.05) say += ' <i>The shortcut is off by ' + Math.abs(hand.p - p).toFixed(2) + ' here: so close to the start or to equivalence it leaves out the acid’s own ionisation or water’s. The exact pH, ' + f2(p) + ', allows for both.</i>';\n if (c.v > BURETTE) say += ' <i>Equivalence needs more than one 50 mL burette: take a smaller aliquot of acid or a stronger NaOH.</i>';\n $('say').innerHTML = say;\n var others = c.fits.filter(function (m) { return m !== n; }).map(function (m) { return m.name.toLowerCase(); });\n var why = c.fits.length\n ? n.name + ' suits it best: its mid-colour, pH ' + ((n.lo + n.hi) / 2).toFixed(1) + ', is nearest the equivalence pH and falls inside the jump, where 0.10 mL (two drops) either side of equivalence takes the pH from ' + f2(c.jump[0]) + ' to ' + f2(c.jump[1]) + '.' + (others.length ? ' Also inside it: ' + others.join(', ') + '.' : '')\n : 'No common indicator reaches its mid-colour within 0.10 mL of equivalence (pH ' + f2(c.jump[0]) + ' to ' + f2(c.jump[1]) + '); ' + n.name.toLowerCase() + ' comes closest. Add the last millilitre drop by drop, or use a pH meter.';\n $('ind').textContent = why + ' In the flask now: ' + colourIn(n, p) + '.';\n }\n\n function set(v) { s.vb = Math.max(0, Math.min(cache.xmax, Math.round(v / DROP) * DROP)); s.vb = Math.round(s.vb * 100) / 100; render(); }\n\n ['acid', 'ca', 'va', 'cb'].forEach(function (id) {\n $(id).addEventListener('change', function () {\n s[id] = id === 'acid' ? this.value : Number(this.value);\n s.vb = 0; setup(); render();\n });\n });\n $('vb').addEventListener('input', function () { set(Number(this.value)); });\n document.querySelectorAll('[data-add]').forEach(function (b) {\n b.addEventListener('click', function () { set(s.vb + Number(b.getAttribute('data-add'))); });\n });\n $('again').addEventListener('click', function () { set(0); });\n $('ahead').addEventListener('change', function () { s.ahead = this.checked; render(); });\n document.addEventListener('know-tokens', render);\n var lastW = 0;\n if (window.ResizeObserver) new ResizeObserver(function (en) { var w = Math.round(en[0].contentRect.width); if (w !== lastW) { lastW = w; requestAnimationFrame(render); } }).observe(svg.parentNode);\n else window.addEventListener('resize', render);\n\n setup(); render();\n})();"
}
```Chemistry practicalsExperiment 7
8of 11
Data table and calculationsanswer key
Evidence, key section, 2 highlights, 1 note, 286 words
My trial run, 15 September, with the same stock the class will use: 0.100 mol/L sodium hydroxide, standardised by the technician that morning.
Burette readings, vinegar diluted ten times, 25.00 mL aliquots
| Trial | Final reading, mL | Initial reading, mL | Titre, mL |
|---|---|---|---|
| Rough | 21.30 | 0.20 | 21.10 |
| 1 | 20.75 | 0.20 | 20.55 |
| 2 | 21.05 | 0.55 | 20.50 |
| 3 | 20.90 | 0.35 | 20.55 |
Trials 1 to 3 agree within 0.10 mL, so they are concordant; the rough titre is left out of the mean. Mean titre: 20.53 mL.
Burette readings, vinegar diluted ten times, 25.00 mL aliquots
| Trial | Final reading, mL | Initial reading, mL | Titre, mL |
|---|---|---|---|
| Rough | 21.30 | 0.20 | 21.10 |
| 1 | 20.75 | 0.20 | 20.55 |
| 2 | 21.05 | 0.55 | 20.50 |
| 3 | 20.90 | 0.35 | 20.55 |
Trials 1 to 3 agree within 0.10 mL, so they are concordant; the rough titre is left out of the mean. Mean titre: 20.53 mL.
```element table
{
"caption": "Burette readings, vinegar diluted ten times, 25.00 mL aliquots",
"columns": [
{
"label": "Trial",
"align": "left"
},
{
"label": "Final reading, mL",
"align": "right",
"numeric": true
},
{
"label": "Initial reading, mL",
"align": "right",
"numeric": true
},
{
"label": "Titre, mL",
"align": "right",
"numeric": true
}
],
"rows": [
[
"Rough",
"21.30",
"0.20",
"21.10"
],
[
"1",
"20.75",
"0.20",
"20.55"
],
[
"2",
"21.05",
"0.55",
"20.50"
],
[
"3",
"20.90",
"0.35",
"20.55"
]
],
"notes": "Trials 1 to 3 agree within 0.10 mL, so they are concordant; the rough titre is left out of the mean. Mean titre: **20.53 mL**."
}
```Sodium hydroxide used. 0.100 mol/L × 0.02053 L = 2.053 × 10⁻³ mol.
Ethanoic acid in the aliquot. CH₃COOH + NaOH → CH₃COONa + H₂O, one to one, so 2.053 × 10⁻³ mol in 25.00 mL.
Concentration. 2.053 × 10⁻³ mol ÷ 0.02500 L = 0.0821 mol/L in the diluted vinegar, so 0.821 mol/L in the bottle.
Mass per 100 mL. 0.821 mol/L × 60.05 g/mol = 49.3 g/L, which is 4.93 g per 100 mL, close to the 5 per cent on the label.
How it works
Replace the photocopied worksheets with a lab manual that improves every year. Each experiment has the same six parts, and each hazard is a Risk section written from your own risk assessment. Beside the titration sits a model: add sodium hydroxide a drop at a time and watch the pH curve climb, jump and level off.