Unit vector calculator — direction & length — Normalize a nonzero plane vector while retaining direction and distinguish the unit vector from the original magnitude.

HTML for this example

Select the code and use your copy shortcut. Changes in this preview are temporary.

<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Unit vector calculator — direction & length</title>
<style>
:root{font-family:Arial,Helvetica,sans-serif;color:#203944;background:#f4f3ee;line-height:1.5;font-size:16px}*{box-sizing:border-box}body{margin:0}main{max-width:1160px;margin:auto;padding:32px 28px 48px}h1,h2,p{margin:0}h1{font-family:Georgia,serif;font-weight:500;font-size:36px;line-height:1.15}header p{color:#53656b;margin-top:8px}.eyebrow{text-transform:uppercase;letter-spacing:.14em;font-size:12px;font-weight:700;color:#596d73;margin-bottom:7px}.workbench{background:#fff;border:1px solid #d7dedb;margin-top:24px;display:grid;grid-template-columns:1.05fr 1fr;border-radius:16px;overflow:hidden}.answer-side{padding:26px}.result{border-left:4px solid #be4e32;padding-left:18px;margin-bottom:22px;min-height:149px}.result h2{font-size:14px;font-weight:600;color:#7f3d2d}.unit{font-family:Georgia,serif;font-size:clamp(28px,3.2vw,42px);line-height:1.25;overflow-wrap:anywhere;margin:8px 0}.metrics{font-size:16px}.metrics strong{font-size:20px}.hint{font-size:13px;color:#586b73;margin-top:8px}label{display:block;font-weight:700;font-size:14px;margin-bottom:6px}.inputs{display:grid;grid-template-columns:1fr 1fr;gap:14px}input{width:100%;min-width:0;border:1px solid #8a9b9f;border-radius:7px;padding:10px 12px;font:inherit;font-size:20px;color:#203944;background:#fff}input[aria-invalid=true]{border-color:#a62d20;background:#fff8f5}button{font:inherit;font-size:14px;min-height:44px;border:1px solid #b5c2c4;border-radius:7px;background:#fff;color:#203944;padding:9px 13px;cursor:pointer}button:hover{background:#eef3f3}button:focus-visible,input:focus-visible{outline:3px solid #167bb1;outline-offset:3px}.actions{display:flex;gap:10px;margin-top:16px}#compute{background:#203f4b;color:white;border-color:#203f4b;flex:1}#compute:hover{background:#315a69}.presets{display:flex;flex-wrap:wrap;gap:7px;margin-top:9px}.presets button{font-size:13px;padding:7px 9px}.preset-title{font-size:13px;margin-top:18px;color:#53656b}#status{font-size:14px;margin-top:12px;min-height:42px;color:#7e3625}.plot-side{background:#f6f9f8;border-left:1px solid #d7dedb;padding:20px;display:flex;flex-direction:column;justify-content:center}figure{margin:0}svg{display:block;width:100%;height:auto;max-height:430px}svg text{font:13px Arial,sans-serif;fill:#3d535c}.legend{display:flex;gap:18px;font-size:13px;flex-wrap:wrap}.legend span:before{content:'';display:inline-block;width:21px;height:3px;background:#286780;vertical-align:middle;margin-right:7px}.legend span:last-child:before{background:#be4e32}figcaption{font-size:13px;color:#52676e;margin-top:10px}.details{display:grid;grid-template-columns:1fr 1fr;gap:30px;margin-top:30px}h2{font-family:Georgia,serif;font-size:25px;font-weight:500}.trace{padding:17px 0;border-top:1px solid #ccd6d5;margin-top:14px;overflow-wrap:anywhere}.trace p{margin-bottom:9px}.trace b{display:block;font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#60737a}.note{font-size:14px;color:#52676e}.ledger{margin-top:14px;border-top:1px solid #ccd6d5}.entry{padding:12px 0;border-bottom:1px solid #dce3e0;display:grid;grid-template-columns:1fr 1fr;gap:6px 12px;font-size:14px}.entry strong{font-weight:600}.entry small{grid-column:1/-1;color:#52676e}.empty{padding:16px 0;color:#53656b;font-size:14px}footer{border-top:1px solid #ccd6d5;margin-top:28px;padding-top:18px;font-size:13px;color:#53656b;max-width:850px}footer p+p{margin-top:8px}[hidden]{display:none!important}@media(max-width:760px){main{padding:22px 18px}.workbench{grid-template-columns:1fr 1fr}.answer-side{padding:20px}.plot-side{padding:12px}.details{gap:22px}h1{font-size:31px}.unit{font-size:30px}.presets{flex-direction:column}}@media(max-width:600px){main{padding:20px 14px 32px}h1{font-size:29px}header p{font-size:14px}.workbench{display:flex;flex-direction:column;margin-top:18px}.answer-side{padding:20px}.result{min-height:130px;margin-bottom:16px}.unit{font-size:32px}.plot-side{border-left:0;border-top:1px solid #d7dedb;padding:18px}.details{grid-template-columns:1fr}.presets{flex-direction:row}.presets button{flex:1}.metrics{font-size:15px}.legend{gap:12px}svg{max-height:350px}}
</style></head>
<body><main><header><div class="eyebrow">Plane vectors · 2D</div><h1>Keep the direction.<br>Make the length one.</h1><p>Divide each component by the original vector’s magnitude.</p></header>
<section class="workbench" aria-label="Unit vector calculator"><div class="answer-side">
<div class="result" aria-live="polite"><h2>Unit vector u</h2><p class="unit" id="unit-result"></p><p class="metrics" id="original-result"></p><p class="hint" id="length-check"></p></div>
<div class="inputs"><div><label for="x">x component</label><input id="x" inputmode="decimal" value="-8" autocomplete="off" aria-describedby="input-rule status"></div><div><label for="y">y component</label><input id="y" inputmode="decimal" value="6" autocomplete="off" aria-describedby="input-rule status"></div></div>
<p class="hint" id="input-rule">Each component: −1000 to 1000. Decimals accepted.</p><div class="actions"><button type="button" id="compute">Compute unit vector</button><button type="button" id="reset">Reset</button></div>
<p class="preset-title">Compare the starting direction</p><div class="presets"><button type="button" id="preset-base">Original ⟨−8, 6⟩</button><button type="button" id="preset-double">Double ⟨−16, 12⟩</button><button type="button" id="preset-reverse">Reverse ⟨8, −6⟩</button></div><p id="status" role="status"></p>
</div><div class="plot-side"><figure><div class="legend"><span>Original v</span><span>Unit u</span></div><svg id="plot" viewBox="0 0 420 420" role="img" aria-label="Original and unit vector on equal-scale axes"></svg><figcaption id="plot-caption"></figcaption></figure></div></section>
<section class="details"><div><h2>Follow the calculation</h2><div class="trace" id="trace"></div><p class="note">Displays use up to six decimal places (or scientific notation for very small values). The length check uses unrounded components; tolerance: |‖u‖ − 1| ≤ 10⁻¹².</p></div><div><h2>Completed comparisons</h2><p class="note">Earlier calculations stay here when you edit. These are not the current answer.</p><div id="ledger" class="ledger" aria-live="polite"></div><p class="hint">Same unit vector does not mean equal original vectors. Reset clears this session’s comparisons.</p></div></section>
<footer><p>This self-contained HTML companion implements the manuscript’s plane-vector exercise. Values and comparison rows last only while this page is open; nothing is saved. This is a bounded 2D calculation, not a physical force or equipment-sizing tool.</p><p>The plot uses a common origin and equal x/y units. A unit arrow can be tiny beside a long vector; it is never enlarged separately. Endpoint coordinates are also shown in the answer and calculation.</p><p>Normalization uses an equivalent rescaled calculation to avoid underflow. At extremely small magnitudes, floating-point representation can round the original length even before display formatting.</p></footer>
</main><script>
'use strict';
const $=id=>document.getElementById(id); const fields=[$('x'),$('y')]; let history=[];
function fmt(n){if(n===0)return '0';if(Math.abs(n)<0.000001)return n.toExponential(5);return String(Number(n.toFixed(6)));}
function pair(x,y){return '⟨'+fmt(x)+', '+fmt(y)+'⟩';}
function evaluate(a,b){const raw=[a.trim(),b.trim()];const errors=raw.map((s,i)=>!s?'Enter the '+(i?'y':'x')+' component.':!/^[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?$/i.test(s)||!Number.isFinite(Number(s))?'Use a finite number for '+(i?'y':'x')+'.':Math.abs(Number(s))>1000?'The '+(i?'y':'x')+' component must be between −1000 and 1000.':'');if(errors.some(Boolean))return {errors};const [x,y]=raw.map(Number);if(x===0&&y===0)return {errors:['No normalized direction for the zero vector.','']};const m=Math.hypot(x,y);const largest=Math.max(Math.abs(x),Math.abs(y)),sx=x/largest,sy=y/largest,scaledLength=Math.hypot(sx,sy);const ux=sx/scaledLength,uy=sy/scaledLength;return {x,y,m,ux,uy,check:Math.hypot(ux,uy)};}
function validate(){const r=evaluate(fields[0].value,fields[1].value);fields.forEach((f,i)=>f.setAttribute('aria-invalid',String(Boolean(r.errors&&r.errors[i]))));return r;}
function clearCurrent(message){$('unit-result').textContent='Awaiting calculation';$('original-result').textContent='';$('length-check').textContent='';$('trace').textContent='Enter a nonzero vector, then choose Compute.';$('plot').innerHTML='';$('plot-caption').textContent='No current arrows. Compute to plot the validated input.';$('status').textContent=message;}
function edited(){const r=validate();clearCurrent(r.errors?r.errors.filter(Boolean).join(' '):'Inputs changed. Choose Compute to update the answer.');}
function plot(r){const extent=Math.max(Math.abs(r.x),Math.abs(r.y),1)*1.45,scale=155/extent;const px=n=>210+n*scale,py=n=>210-n*scale;let content='<defs><marker id="head-v" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10" fill="#286780"/></marker><marker id="head-u" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10" fill="#be4e32"/></marker></defs>';
for(let i=-2;i<=2;i++){let p=210+i*77.5;content+='<path d="M55 '+p+'H365 M'+p+' 55V365" stroke="#e0e8e5" fill="none"/>';if(i!==0){content+='<text x="'+p+'" y="230" text-anchor="middle">'+fmt(i*extent/2)+'</text><text x="202" y="'+(420-p+4)+'" text-anchor="end">'+fmt(i*extent/2)+'</text>';}}
content+='<path d="M40 210H380 M210 40V380" stroke="#8ba3aa"/><text x="384" y="215">x</text><text x="217" y="35">y</text><text x="216" y="226">0</text>';
content+='<line id="arrow-v" x1="210" y1="210" x2="'+px(r.x)+'" y2="'+py(r.y)+'" stroke="#286780" stroke-width="3" stroke-dasharray="6 3" marker-end="url(#head-v)"/><line id="arrow-u" x1="210" y1="210" x2="'+px(r.ux)+'" y2="'+py(r.uy)+'" stroke="#be4e32" stroke-width="2.5" marker-end="url(#head-u)"/>';
content+='<text x="'+px(r.x)+'" y="'+(py(r.y)-14)+'" text-anchor="middle">v '+pair(r.x,r.y)+'</text><path d="M'+px(r.ux)+' '+py(r.uy)+' L288 322 L350 322" fill="none" stroke="#be4e32" stroke-width="1"/><text x="250" y="345">u '+pair(r.ux,r.uy)+'</text>';$('plot').innerHTML=content;$('plot-caption').textContent='Both start at (0, 0). Each grid interval = '+fmt(extent/2)+' units on both axes.';}
function ledger(){const el=$('ledger');el.innerHTML='';if(!history.length){el.textContent='No completed calculations.';el.className='ledger empty';return;}el.className='ledger';history.forEach((r,i)=>{const row=document.createElement('div');row.className='entry';const a=document.createElement('strong');a.textContent='v = '+pair(r.x,r.y);const b=document.createElement('span');b.textContent='u = '+pair(r.ux,r.uy);const c=document.createElement('small');const dot=(-.8*r.ux+.6*r.uy);const relation=Math.abs(dot-1)<=1e-12?'Same direction as ⟨−8, 6⟩':Math.abs(dot+1)<=1e-12?'Opposite direction to ⟨−8, 6⟩':'Different direction from ⟨−8, 6⟩';c.textContent='‖v‖ = '+fmt(r.m)+' · '+relation;row.append(a,b,c);el.append(row);});}
function compute(){const r=validate();if(r.errors){clearCurrent(r.errors.filter(Boolean).join(' '));return;}$('unit-result').textContent='u = '+pair(r.ux,r.uy);$('original-result').textContent='Original v = '+pair(r.x,r.y)+' · ‖v‖ = '+fmt(r.m);$('length-check').textContent='Unit-length check: '+fmt(r.check)+' · '+(Math.abs(r.check-1)<=1e-12?'within tolerance':'outside tolerance');$('trace').innerHTML='';const lines=[['1 · Find the original length','‖v‖ = √(('+fmt(r.x)+')² + ('+fmt(r.y)+')²) = '+fmt(r.m)],['2 · Divide by that length','u = ⟨'+fmt(r.x)+' / '+fmt(r.m)+', '+fmt(r.y)+' / '+fmt(r.m)+'⟩ = '+pair(r.ux,r.uy)],['3 · Check the unit length','‖u‖ = √(('+fmt(r.ux)+')² + ('+fmt(r.uy)+')²) ≈ '+fmt(r.check)+' (using unrounded values)']];lines.forEach(([label,text])=>{const p=document.createElement('p'),b=document.createElement('b');b.textContent=label;p.append(b,document.createTextNode(text));$('trace').append(p);});plot(r);history.push(r);ledger();$('status').textContent='Computed. Added to completed comparisons.';}
fields.forEach(f=>{f.addEventListener('input',edited);f.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.isComposing&&e.keyCode!==229){e.preventDefault();compute();}});});$('compute').addEventListener('click',compute);[['preset-base',-8,6],['preset-double',-16,12],['preset-reverse',8,-6]].forEach(([id,x,y])=>$(id).addEventListener('click',()=>{fields[0].value=x;fields[1].value=y;edited();}));$('reset').addEventListener('click',()=>{fields[0].value='-8';fields[1].value='6';history=[];validate();ledger();clearCurrent('');});compute();
</script></body></html>