In this hands-on guide, you’ll build an aspect ratio calculator in JS with clean, accessible code and a polished UI. We’ll cover ratio math, input validation, the modern CSS aspect-ratio
property, and progressive enhancement—plus a live demo you can drop into any project.
What is aspect ratio?
Aspect ratio is the proportional relationship between width and height, typically written as width:height
(e.g., 16:9, 4:3, 1:1). Keeping the ratio consistent prevents stretched or squashed visuals when resizing images, videos, canvases, and containers.
- 16:9 → Widescreen video, YouTube, modern displays
- 4:3 → Legacy displays, some document cams
- 1:1 → Square posts and avatars
- 3:2 → Photography (many camera sensors)
- 21:9 → Ultra-wide screens
Formula: Given ratio a:b
, for a known width W
, height H = W × (b / a)
. For a known height H
, width W = H × (a / b)
.
How the calculator works
Our calculator supports three workflows:
- Width + Height → Ratio (simplifies using the Euclidean algorithm)
- Ratio + Width → Height (with rounding options)
- Ratio + Height → Width (with rounding options)
It accepts ratios like 16:9
, 4/3
, or a decimal like 1.7778
(interpreted as 1.7778:1
). When inputs contain decimals, the calculator scales them to integers internally, then reduces to the simplest form (e.g., 1920×1080
→ 16:9
).
Build it yourself (HTML, CSS, JS)
1) HTML
<div id="ar-calc">...</div>
2) CSS (scoped)
.ar-card{max-width:860px;margin:1.5rem 0;padding:1rem;border:1px solid #e5e7eb;border-radius:12px;background:#fff;box-shadow:0 4px 18px rgba(0,0,0,.04)}.ar-row{display:flex;gap:.75rem;align-items:center;margin-bottom:1rem}.ar-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.ar-field{display:flex;flex-direction:column}.ar-label{font-weight:600;margin-bottom:.25rem}.ar-input{padding:.6rem .75rem;border:1px solid #d1d5db;border-radius:10px}.ar-help{color:#6b7280}.ar-quick{margin-top:.5rem;display:flex;gap:.5rem;flex-wrap:wrap}.ar-chip{border:1px solid #d1d5db;background:#f9fafb;border-radius:999px;padding:.25rem .6rem;cursor:pointer}.ar-actions{display:flex;gap:.5rem;margin-top:1rem}.ar-btn{background:#0ea5e9;color:#fff;border:none;border-radius:10px;padding:.6rem 1rem;cursor:pointer}.ar-btn--ghost{background:#f3f4f6;color:#111827}.ar-btn--sm{padding:.4rem .6rem;font-size:.9rem}.ar-alert{margin:.75rem 0;color:#b91c1c;background:#fee2e2;border:1px solid #fecaca;border-radius:10px;padding:.5rem .75rem}.ar-result{margin-top:1rem;border-top:1px dashed #e5e7eb;padding-top:1rem}.ar-result__row{margin:.4rem 0}.ar-preview{margin-top:1rem;border:1px dashed #cbd5e1;border-radius:12px;padding:.75rem;background:#f8fafc}.ar-frame{width:100%;aspect-ratio:16/9;background:repeating-conic-gradient(#fff 0 25%, #f1f5f9 0 50%) 50% /20px 20px;border:2px solid #94a3b8;border-radius:12px;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}.ar-frame #ar-frame-label{background:rgba(15,23,42,.85);color:#fff;padding:.25rem .5rem;border-radius:6px;font-size:.9rem}.ar-toc{border:1px solid #e5e7eb;border-radius:10px;padding:.75rem;background:#f8fafc;margin:1rem 0}.ar-list{margin:.5rem 0 1rem 1.15rem}
@media (max-width:640px){.ar-grid{grid-template-columns:1fr}}
3) JavaScript (logic)
// Utility
const $ = (s, root=document) => root.querySelector(s);
const $$ = (s, root=document) => [...root.querySelectorAll(s)];
function decimals(n){ const s = String(n); const i = s.indexOf('.'); return i >= 0 ? (s.length - i - 1) : 0; }
function gcdInt(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ [a,b]=[b,a%b]; } return a||1; }
function toInt(n, scale=1){ return Math.round(Number(n)*scale); }
function simplifyRatio(w, h){
const d = Math.max(decimals(w), decimals(h));
const scale = Math.pow(10, d);
const iw = toInt(w, scale), ih = toInt(h, scale);
const g = gcdInt(iw, ih);
return [iw/g, ih/g];
}
function parseRatio(input){
if(!input) return null;
const s = String(input).trim();
let a,b;
if(s.includes(':')) [a,b] = s.split(':');
else if(s.includes('/')) [a,b] = s.split('/');
else { a = s; b = 1; } // decimal like "1.7778" => 1.7778:1
a = Number(a); b = Number(b);
if(!isFinite(a) || !isFinite(b) || a <= 0 || b <= 0) return null;
const [sa,sb] = simplifyRatio(a,b);
return { a: sa, b: sb, decimal: a/b };
}
function roundTo(n, mode='nearest'){
if(mode==='floor') return Math.floor(n);
if(mode==='ceil') return Math.ceil(n);
return Math.round(n);
}
function commonName(a,b){
const map = { "16:9": "Widescreen", "4:3": "Classic", "1:1": "Square", "3:2": "Photo", "21:9": "Ultrawide" };
const key = `${a}:${b}`;
return map[key] ? ` (${map[key]})` : "";
}
// App
(function initCalc(){
const root = $("#ar-calc");
if(!root) return;
const modeEl = $("#ar-mode", root);
const ratioEl = $("#ar-ratio", root);
const widthEl = $("#ar-width", root);
const heightEl = $("#ar-height", root);
const roundEl = $("#ar-round", root);
const alertEl = $("#ar-alert", root);
const resultEl = $("#ar-result", root);
const outRatio = $("#ar-out-ratio", root);
const outSize = $("#ar-out-size", root);
const outDecimal = $("#ar-out-decimal", root);
const outCSS = $("#ar-out-css", root);
const copyCSS = $("#ar-copy-css", root);
const frame = $("#ar-frame", root);
const frameLabel = $("#ar-frame-label", root);
function setVisible(){
const mode = modeEl.value;
// Show/hide fields based on mode
root.querySelector('[data-field="ratio"]').style.display = (mode==='wh-to-ratio') ? 'none' : '';
root.querySelector('[data-field="width"]').style.display = (mode==='ratio-height-to-width') ? 'none' : '';
root.querySelector('[data-field="height"]').style.display = (mode==='ratio-width-to-height') ? 'none' : '';
}
function showError(msg){
alertEl.textContent = msg;
alertEl.hidden = !msg;
resultEl.hidden = !!msg;
}
function showResult({a,b,W,H}){
const dec = (W && H) ? (W/H) : (a/b);
const decTxt = (dec>1) ? `${dec.toFixed(4)}:1` : `1:${(1/dec).toFixed(4)}`;
outRatio.textContent = `${a}:${b}${commonName(a,b)}`;
outSize.textContent = (W && H) ? `${W} × ${H} px` : `—`;
outDecimal.textContent = decTxt;
outCSS.textContent = `aspect-ratio: ${a} / ${b};`;
resultEl.hidden = false;
alertEl.hidden = true;
// Preview
frame.style.aspectRatio = `${a} / ${b}`;
frameLabel.textContent = (W && H) ? `${W}×${H}px (${a}:${b})` : `${a}:${b}`;
}
// Quick ratio chips
$$(".ar-chip", root).forEach(btn => {
btn.addEventListener("click", () => {
ratioEl.value = btn.getAttribute("data-ratio");
ratioEl.focus();
});
});
$("#ar-calc-btn", root).addEventListener("click", () => {
const mode = modeEl.value;
const rounding = roundEl.value;
if(mode === "wh-to-ratio"){
const W = Number(widthEl.value), H = Number(heightEl.value);
if(!(W>0 && H>0)) return showError("Please enter positive width and height.");
const [a,b] = simplifyRatio(W,H);
showResult({a,b,W,H});
return;
}
// Modes with ratio
const pr = parseRatio(ratioEl.value);
if(!pr) return showError("Invalid ratio. Try formats like 16:9, 4/3, or 1.7778");
const {a,b} = pr;
if(mode === "ratio-width-to-height"){
const W = Number(widthEl.value);
if(!(W>0)) return showError("Enter a positive width.");
const Hraw = W * (b / a);
const H = roundTo(Hraw, rounding);
showResult({a,b,W,H});
return;
}
if(mode === "ratio-height-to-width"){
const H = Number(heightEl.value);
if(!(H>0)) return showError("Enter a positive height.");
const Wraw = H * (a / b);
const W = roundTo(Wraw, rounding);
showResult({a,b,W,H});
return;
}
});
$("#ar-reset-btn", root).addEventListener("click", () => {
[ratioEl.value, widthEl.value, heightEl.value] = ["", "", ""];
showError("");
resultEl.hidden = true;
frame.style.aspectRatio = "16 / 9";
frameLabel.textContent = "Preview";
});
copyCSS.addEventListener("click", async () => {
try { await navigator.clipboard.writeText(outCSS.textContent); copyCSS.textContent = "Copied!"; setTimeout(() => (copyCSS.textContent="Copy CSS"), 1200); }
catch{ alert("Copy failed. Select the text and copy manually."); }
});
modeEl.addEventListener("change", setVisible);
setVisible();
})();
Bonus: To lock UI containers, you can also apply the modern CSS property directly—e.g., aspect-ratio: 3 / 2;
—without JavaScript. The JS calculator is still invaluable for deriving consistent pixel dimensions, thumbnails, or export sizes.
Best practices & edge cases
- Decimals in inputs: The calculator scales values to integers and reduces properly (e.g.,
1280.5×720.3
→ simplified ratio). - Rounding control: Use nearest for general UI, floor to avoid overflow, ceil to avoid under-fill in media pipelines.
- Accessibility: Every input is labeled; results announce via
aria-live
; buttons are keyboard-friendly. - Responsive preview: The frame uses
aspect-ratio
so users can “see” the shape at any screen size. - Common mappings: 1920×1080 → 16:9; 1280×960 → 4:3; 1080×1080 → 1:1; 3000×2000 → 3:2.
- Object fit: For images/videos inside fixed ratio boxes, pair with
object-fit: cover|contain
to control cropping or letterboxing.
FAQ
How do I convert width and height to a clean ratio?
Use the Euclidean algorithm to divide both numbers by their greatest common divisor (GCD). For example, 1920×1080 → GCD 120 → 16:9.
What if my ratio is a decimal like 1.7778?
Treat it as 1.7778:1
. The calculator will scale and reduce it to a simple fraction (e.g., ~16:9).
Should I use CSS or JS to enforce aspect ratios?
Use CSS aspect-ratio
to size containers. Use JS to derive pixel-perfect exports or when you must compute missing dimensions.