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:

  1. Width + Height → Ratio (simplifies using the Euclidean algorithm)
  2. Ratio + Width → Height (with rounding options)
  3. 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×108016: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.