GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

CSS Calculator Design: 9 Inputs, Unit Converter UI

Among nine css calculator design inputs, one draws a QR grid from a text value, one fills a ring gauge from a birthdate value, and one redraws a time value

Auto-plays · click a tile to jump to its section · all nine in one zip

Nine ways to turn a single typed number into a picture sit in this set, and every one keeps that picture attached to the field you're editing: a Celsius value that slides a handle up a gradient scale, a byte count that decides its own unit before filling a badge, a birthdate that fills a ring by the day, a clock time that redraws four labeled cards at once, a hex value that crosses a pass line on a badge, a pair of geographic fields that stay in sync no matter which one you touch, a string that gets hashed into a QR-style grid on canvas, a bill split among however many people a headcount value gives, and a length that pushes two rulers together. None of the nine reach for a full html css calculator design kit or a math library — every pattern below is one input listener, one small named function, and a target/rendered pair that requestAnimationFrame nudges into place.

Type a Value, Watch Pattern 01 Answer

Gradient Thermometer Input Filled by a Temperature Value
Open in a new tab

01 · Gradient Thermometer Input Filled by a Temperature Value — Measured area 2.117% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LERP_RATEcelsiusToFahrenheitrenderedT

Pattern 01 above needs nothing beyond a celsiusToFahrenheit function and a renderedT value that requestAnimationFrame eases toward whatever applyValue just computed from the typed Celsius reading. The remaining eight patterns sit further down, each next to its own code block.

How the Nine Calculator Inputs Compare, Measured

# Pattern Cumulative area Avg. intensity Frames moved
01 Gradient thermometer input 2.117% 119.9 23/23
02 File-size unit badge 13.481% 113.3 23/23
03 Birthdate age ring 2.981% 122.3 23/23
04 Timezone card stack 9.295% 93.0 9/23
05 Color contrast badge 4.078% 109.2 23/23
06 Geo coordinate sync 3.172% 151.9 23/23
07 Text-to-QR canvas 4.272% 202.8 23/23
08 Tip-split ring 3.385% 116.8 23/23
09 Length unit scale 5.763% 123.3 23/23

Pattern 02's badge covers the most area of the nine at 13.481%, well ahead of pattern 01's thermometer handle at 2.117%, the smallest in the set. Pattern 04 stands apart in the last column: only 9 of its 23 captured frames counted as moved, well under the 23/23 the other eight patterns reach.

One field driving one shape — 01, 02, 05, 07

A single typed value feeds one visual in these four: a temperature into a handle position, a byte count into a fill ratio, a hex code into a pass/fail badge, a string into a hashed grid.

Two typed fields folded into one number — 03, 08

A birthdate plus a reference date becomes an age and a countdown in 03; a total plus a headcount becomes a per-person share in 08 — both need two fields before either output means anything.

One value driving several outputs, or two fields kept in sync — 04, 06, 09

Pattern 04 spreads one typed time across four cards at once. Patterns 06 and 09 go the other way: editing either of two paired fields writes the other one back, so neither field is ever the "real" source.

Nine Small Functions, Each With Its Own requestAnimationFrame Loop

Every code block below is a contiguous slice straight out of that pattern's index.html — the named calculation function or two, plus the constant right above them where one exists.

01Gradient Thermometer Input Filled by a Temperature Value

Typing a Celsius value into pattern 01 computes the matching Fahrenheit reading with celsiusToFahrenheit and eases a handle up a gradient scale toward that value's position. LERP_RATE sets how quickly renderedT catches up to a freshly typed target, so a lower rate makes the handle trail further behind a fast edit.

Gradient Thermometer Input Filled by a Temperature Value
Open in a new tab

01 · Gradient Thermometer Input Filled by a Temperature Value — Measured area 2.117% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LERP_RATEcelsiusToFahrenheitrenderedT
  function celsiusToFahrenheit(c) { return c * 9 / 5 + 32; }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = 'Celsius ', SENT_MID = ' degrees is Fahrenheit ', SENT_SUFFIX = ' degrees.';

  var targetT = 0, renderedT = 0;
  var LERP_RATE = 0.14; // variant 01: lowering this makes the handle trail further behind a value change

  function readValue() {
    var c = parseFloat(input.value);
    return isNaN(c) ? 0 : c;
  }

  function applyValue(c) {
    var f = celsiusToFahrenheit(c);
    targetT = clamp01((c - MIN_C) / (MAX_C - MIN_C));
    readout.textContent = SENT_PREFIX + c.toFixed(1) + SENT_MID + f.toFixed(1) + SENT_SUFFIX;
    if (reduce) { renderedT = targetT; paint(); }
  }

  function paint() {
    var trackH = scale.clientHeight - handle.offsetHeight;
    var y = (1 - renderedT) * trackH;
    handle.style.transform = 'translateY(' + y.toFixed(1) + 'px)';
  }

02File-size Badge That Picks Its Own Unit From a Byte Value

digitsOf counts the digits in a typed byte value and pickUnit uses that count to choose B, KB, MB, or GB, then fillRatio scales the value inside that unit to fill a bar. Because the unit switch happens at fixed digit counts, the bar's fill can drop sharply the moment a value crosses into a new unit — more on that further down.

File-size Badge That Picks Its Own Unit From a Byte Value
Open in a new tab

02 · File-size Badge That Picks Its Own Unit From a Byte Value — Measured area 13.481% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

pickUnitdigitsOffillRatio
  function fillRatio(bytes, unit) {
    var v = bytes / Math.pow(1024, unit.pow);
    return clamp01(v / 1024);
  }

  var SENT_PREFIX = 'The entered ', SENT_MID1 = ' bytes, in ', SENT_MID2 = ', equal ', SENT_SUFFIX = '.';

  var targetR = 0, renderedR = 0;
  var LERP_RATE = 0.16; // variant 02: lowering this makes the bar trail further behind a value change

  function readValue() {
    var n = parseFloat(input.value);
    return isNaN(n) || n < 0 ? 0 : n;
  }

  function applyValue(bytes) {
    var unit = pickUnit(bytes);
    var valueInUnit = bytes / Math.pow(1024, unit.pow);
    targetR = fillRatio(bytes, unit);
    unitEl.textContent = unit.name;
    readout.textContent = SENT_PREFIX + bytes + SENT_MID1 + unit.name + SENT_MID2 + valueInUnit.toFixed(2) + SENT_SUFFIX;
    if (reduce) { renderedR = targetR; paint(); }
  }

03D-day Ring Filled From a Birthdate Value

Both a typed birthdate and a typed reference date feed daysBetween, which works from Date.UTC on the year/month/day parts rather than the browser's current clock, so the ring reads the same way no matter when the page loads. ageAt and nextBirthdayGap split that day count into an age and a countdown, and the ring fills by how close the next birthday sits.

D-day Ring Filled From a Birthdate Value
Open in a new tab

03 · D-day Ring Filled From a Birthdate Value — Measured area 2.981% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

ageAtnextBirthdayGapdaysBetween
  function daysBetween(a, b) {
    var msPerDay = 86400000;
    var da = Date.UTC(a.y, a.m - 1, a.d);
    var db = Date.UTC(b.y, b.m - 1, b.d);
    return Math.round((db - da) / msPerDay);
  }
  // one layer of calculation — from a birthdate and a reference date, get the age and days to the next birthday, then a ring-fill ratio
  function ageAt(birth, ref) {
    var age = ref.y - birth.y;
    var hadBirthdayThisYear = (ref.m > birth.m) || (ref.m === birth.m && ref.d >= birth.d);
    if (!hadBirthdayThisYear) age -= 1;
    return age;
  }
  function nextBirthdayGap(birth, ref) {
    var nextY = ref.y;
    var thisYearBday = { y: ref.y, m: birth.m, d: birth.d };
    var gapThisYear = daysBetween(ref, thisYearBday);
    if (gapThisYear < 0) { nextY = ref.y + 1; }
    var target = { y: nextY, m: birth.m, d: birth.d };
    return daysBetween(ref, target);
  }

  var SENT_PREFIX = 'You are ', SENT_MID = ' years old, and the next birthday is in ', SENT_SUFFIX = ' days.';

  var targetT = 0, renderedT = 0;

04Timezone Cards Split From a Single Time Value

One typed time value runs through addOffsetMinutes once per card, each with its own fixed UTC offset in hours rather than a city name, and isDaytime/brightnessOf dim or brighten each card to match. Only the offset numbers ever change, so no card claims to know a real place — just a UTC+9 or UTC-8 label.

Timezone Cards Split From a Single Time Value
Open in a new tab

04 · Timezone Cards Split From a Single Time Value — Measured area 9.295% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

addOffsetMinutesbrightnessOfisDaytime
  function pad2(n) { return (n < 10 ? '0' : '') + n; }
  function formatOffset(off) {
    var sign = off >= 0 ? '+' : '-';
    var abs = Math.abs(off);
    var h = Math.floor(abs);
    var m = Math.round((abs - h) * 60);
    return 'UTC' + sign + h + (m ? ':' + pad2(m) : '');
  }
  // one layer of calculation — add an offset in minutes to a base time in minutes for each card, then judge day/night for brightness
  function addOffsetMinutes(baseMinutes, offsetHours) {
    var total = (baseMinutes + offsetHours * 60) % 1440;
    if (total < 0) total += 1440;
    return total;
  }
  function isDaytime(minutes) { return minutes >= 360 && minutes < 1080; }
  function brightnessOf(minutes) {
    var distFromNoon = Math.abs(minutes - 720) / 720;
    return clamp01(1 - distFromNoon * 0.7);
  }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = 'At reference ', SENT_MID = ' time, the first card shows ', SENT_SUFFIX = '.';

  var targets = cards.map(function () { return 0; });
  var rendered = cards.map(function () { return 0; });

05Contrast Badge That Crosses a Pass Line From a Hex Value

hexToRgb and relativeLuminance turn a typed hex value into the gamma-corrected luminance the WCAG formula expects, and contrastRatio checks it against both white and black to find the better pairing. The badge only flips to a pass state once the better of those two ratios clears 4.5:1.

Contrast Badge That Crosses a Pass Line From a Hex Value
Open in a new tab

05 · Contrast Badge That Crosses a Pass Line From a Hex Value — Measured area 4.078% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

hexToRgbrelativeLuminancecontrastRatio
  var PASS_THRESHOLD = 4.5; // WCAG AA body-text threshold

  function hexToRgb(hex) {
    var m = /^#?([0-9a-f]{6})$/i.exec(hex || '');
    if (!m) return { r: 255, g: 91, b: 60 };
    var n = parseInt(m[1], 16);
    return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
  }
  // one layer of calculation — sRGB gamma-corrected relative luminance, then the WCAG contrast formula
  function relativeLuminance(rgb) {
    function chan(c) {
      var v = c / 255;
      return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
    }
    return 0.2126 * chan(rgb.r) + 0.7152 * chan(rgb.g) + 0.0722 * chan(rgb.b);
  }
  function contrastRatio(l1, l2) {
    var hi = Math.max(l1, l2), lo = Math.min(l1, l2);
    return (hi + 0.05) / (lo + 0.05);
  }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = 'White-background contrast ', SENT_MID1 = ':1 · black-background contrast ', SENT_MID2 = ':1 — ', SENT_SUFFIX = ' is better.';

  var targetX = 0, renderedX = 0;

06Coordinate Input Synced Between Two Notations From One Value

Editing the degrees, minutes, or seconds fields runs dmsToDecimal and writes the result into the decimal field; editing the decimal field runs decimalToDms the other way. Either direction ends at the same paintCommon call, so the marker on the scale always matches whichever field was typed last.

Coordinate Input Synced Between Two Notations From One Value
Open in a new tab

06 · Coordinate Input Synced Between Two Notations From One Value — Measured area 3.172% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

dmsToDecimaldecimalToDmsapplyFromDecimal
  // one layer of calculation — degrees/minutes/seconds to decimal and back, then a position on the scale (0 to 1)
  function dmsToDecimal(deg, min, sec) {
    var sign = deg < 0 ? -1 : 1;
    return sign * (Math.abs(deg) + min / 60 + sec / 3600);
  }
  function decimalToDms(dec) {
    var sign = dec < 0 ? -1 : 1;
    var abs = Math.abs(dec);
    var deg = Math.floor(abs);
    var minFull = (abs - deg) * 60;
    var min = Math.floor(minFull);
    var sec = (minFull - min) * 60;
    return { deg: sign * deg, min: min, sec: sec };
  }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = 'DMS ', SENT_MID = ' converts to decimal ', SENT_SUFFIX = ' degrees.';

  var targetX = 0, renderedX = 0;
  var LERP_RATE = 0.15;

  function paintCommon(dec) {
    targetX = clamp01((dec + RANGE) / (RANGE * 2));
    if (reduce) { renderedX = targetX; paint(); }
  }

07QR Canvas Scattered Into a Grid From a Text Value

hashStr folds a typed text value into one integer, and cellOn mixes that hash with each cell's index to decide black or white — it is a QR-style grid, not a real, scannable QR code. GRID_MAX caps how many cells computeGrid will ever draw across, so grids stop growing once the typed text gets long enough.

QR Canvas Scattered Into a Grid From a Text Value
Open in a new tab

07 · QR Canvas Scattered Into a Grid From a Text Value — Measured area 4.272% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

hashStrcellOncomputeGrid
  function hashStr(s) {
    var h = 2166136261;
    for (var i = 0; i < s.length; i++) {
      h ^= s.charCodeAt(i);
      h = (h * 16777619) >>> 0;
    }
    return h >>> 0;
  }
  function cellOn(hash, i) {
    var x = (hash ^ Math.imul(i + 1, 2654435761)) >>> 0;
    x = (x ^ (x >>> 15)) >>> 0;
    return (x & 1) === 1;
  }
  var GRID_MAX = 12; // variant 07: raising this makes the grid draw more cells across once the text gets long

08Tip-split Ring That Divides a Share From a Headcount Value

perPersonShare divides a typed total by a typed headcount, and shareFraction turns that headcount into the ring's fill fraction, so the ring always reads 1 divided by however many people are entered. formatNumber only touches the display string — it re-inserts thousands separators without changing the math underneath.

Tip-split Ring That Divides a Share From a Headcount Value
Open in a new tab

08 · Tip-split Ring That Divides a Share From a Headcount Value — Measured area 3.385% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

perPersonShareshareFractionformatNumber
  function shareFraction(n) { return n > 0 ? 1 / n : 0; }
  function formatNumber(v) {
    var s = Math.round(v).toString();
    var out = '';
    for (var i = 0; i < s.length; i++) {
      var pos = s.length - i;
      out = s[i] + out;
      if (pos > 1 && (pos - 1) % 3 === 0) out += ',';
    }
    return out;
  }

  var SENT_PREFIX = 'Total ', SENT_MID1 = ' split among ', SENT_MID2 = ' people gives ', SENT_MID3 = ' per person, which is ', SENT_SUFFIX = '% of the total.';

  var targetF = 0, renderedF = 0;
  var LERP_RATE = 0.15;

  function applyValue(total, n) {
    var share = perPersonShare(total, n);
    var frac = shareFraction(n);
    targetF = frac;
    shareEl.textContent = '1/' + n;
    readout.textContent = SENT_PREFIX + formatNumber(total) + SENT_MID1 + n + SENT_MID2 + formatNumber(share) + SENT_MID3 + (frac * 100).toFixed(1) + SENT_SUFFIX;
    if (reduce) { renderedF = targetF; paint(); }
  }

09Unit Scale Where a Length Value Pushes Two Rulers Together

Typing into the centimeter field runs cmToInch and moves both markers via applyFromCm; typing into the inch field runs inchToCm the other way. Both rulers share one LERP_RATE, so a typed change always reaches its new position on both scales at the same pace.

Unit Scale Where a Length Value Pushes Two Rulers Together
Open in a new tab

09 · Unit Scale Where a Length Value Pushes Two Rulers Together — Measured area 5.763% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

cmToInchinchToCmapplyFromCm
  var CM_MAX = 60, IN_MAX = 24;
  // one layer of calculation — cm to inch and back, then a position on each ruler (0 to 1)
  function cmToInch(cm) { return cm / 2.54; }
  function inchToCm(inch) { return inch * 2.54; }
  function clamp01(v) { return v < 0 ? 0 : v > 1 ? 1 : v; }

  var SENT_PREFIX = 'Centimeters ', SENT_MID = ' convert to inches ', SENT_SUFFIX = '.';

  var targetCm = 0, renderedCm = 0, targetIn = 0, renderedIn = 0;
  var LERP_RATE = 0.15;

  function readSentence(cm, inch) {
    readout.textContent = SENT_PREFIX + cm.toFixed(1) + SENT_MID + inch.toFixed(2) + SENT_SUFFIX;
  }

  function applyFromCm() {
    var cm = parseFloat(cmInput.value); if (isNaN(cm) || cm < 0) cm = 0;
    var inch = cmToInch(cm);
    inInput.value = inch.toFixed(2);
    targetCm = clamp01(cm / CM_MAX);
    targetIn = clamp01(inch / IN_MAX);
    readSentence(cm, inch);
    if (reduce) { renderedCm = targetCm; renderedIn = targetIn; paint(); }
  }

Is There a css input design example Collection Like This One Somewhere?

Most css input design example pages online turn up as either a single static mockup or a color picker with no calculation behind it at all. The nine patterns above sit in a different spot: a small named function reads a typed value on every input/change event, and a requestAnimationFrame loop eases a rendered value toward whatever that function just produced — no charting library, no date-math package, no CDN script, just the zip below and a browser.

Where a Calculator-style Input Actually Earns Its Keep

A file upload form rarely needs to show a raw byte count — pattern 02's unit-picking badge matches what most upload widgets show once a file is queued, and only the demo's own input value would need to be swapped for whatever number the real upload step already holds.

A meeting scheduler for a distributed team is the more literal use for pattern 04 — typing one proposed time and reading four offset-labeled cards back is faster than opening four separate clocks, though a production version would need real participant offsets rather than four fixed ones. Pattern 08's per-person split fits the same slot inside a receipt-splitting screen: type a total and a headcount, and the ring communicates the split instantly instead of making someone do the division by hand.

Pattern 09's paired rulers solve a small unit converter ui design problem the same way pattern 06's paired coordinate fields do — neither field should ever look more "correct" than the other, since a person editing centimeters and a person editing inches are both just as likely to be the one who's right.

Where It Breaks — a Digit Boundary in Pattern 02

Type 999 into pattern 02 and the badge reads roughly 97.6% full, since digitsOf still counts three digits and pickUnit keeps it in the B unit. Type 1000 — one byte more — and digitsOf now counts four digits, pickUnit switches the unit to KB, and fillRatio divides the same value by 1024 in the new unit, so the bar drops to well under 1% full even though the underlying byte count barely changed. Nothing in the code is wrong here; the bar was only ever built to read as "how close to filling this particular unit," and a unit switch resets what "full" means without any transition to soften it.

All nine also check prefers-reduced-motion once at load: when it's on, every applyValue/paintCommon call sets the rendered value straight to its target and skips the frame loop, so a reduced-motion visitor sees the finished ring, bar, or grid immediately instead of watching it ease in. Nine small functions and nine listeners are short enough to read end to end in the files themselves, which is the fastest way to check that behavior — the zip below opens with the password eaaz4gef.

What Happens When LERP_RATE or GRID_MAX Move

Eight of the nine patterns share the same shape: a target-style value gets set the moment a field fires input or change, and a frame loop nudges a rendered-style value toward it by LERP_RATE on every tick. That rate sits between 0.13 (pattern 03's ring) and 0.17 (pattern 05's contrast badge) across the set, so the ring is the slowest of the eight to settle and the contrast badge the quickest. Pattern 07 skips that shape entirely — instead of easing a rendered value toward a target, computeGrid reads the whole typed string and works out every cell's value in one synchronous pass, then draw reveals those cells on canvas at a fixed 90-per-second rate, so a longer string means more cells and a correspondingly longer reveal, not a faster or slower catch-up rate.

01 — LERP_RATE

Lowering this from 0.14 to 0.08 makes the gradient handle trail further behind a fast Celsius edit, so a quick typed change is visibly still catching up a moment after the keystroke instead of landing right away.

03 — LERP_RATE

Lowering this from 0.13 to 0.07 slows the age ring the same way — already the slowest-settling of the eight lerp-based patterns at its base rate, cutting it roughly in half stretches that settle time out further still.

07 — GRID_MAX

Raising this from 12 to 18 lets computeGrid draw up to 18 cells across instead of 12 once a typed string is long enough to need it, so long strings get a visibly denser grid instead of being capped at the same 12-wide pattern as a short one.

Variant Changed value What it feels like
01 LERP_RATE 0.14 → 0.08 Thermometer easing rate roughly halves The handle takes noticeably longer to arrive at a freshly typed temperature instead of catching up almost immediately
03 LERP_RATE 0.13 → 0.07 Age-ring easing rate roughly halves The ring's fill trails a newly typed birthdate or reference date for longer before settling into its final arc
07 GRID_MAX 12 → 18 Grid cell cap rises from 12 to 18 Long typed strings draw a denser 18-wide grid instead of being capped at 12, while short strings look about the same either way

Three Variants, and What reduced-motion Removes

The table above already covers the three confirmed variants — 01's and 03's LERP_RATE, and 07's GRID_MAX — and each one edits exactly that single constant while every listener and the prefers-reduced-motion branch stays identical to its base pattern. A variant's index.html is the base file with only that one number changed, so the accessibility behavior documented above needs no re-checking per variant.

FAQ

Is pattern 07's grid a real QR code I can scan?

No — hashStr and cellOn produce a QR-style pattern of black and white cells from a deterministic hash of the typed text, not an actual QR encoding, so no scanner will read anything meaningful out of it.

Why does pattern 04 label its cards with UTC offsets instead of city names?

Because the demo only ever computes an offset in minutes through addOffsetMinutes — it never looks up which city sits at which offset, so labeling a card with a real city would claim more than the code actually knows.

Do any of these nine need a math or date library to run?

No — an input field design css codepen might reach for a date-math package or a big-number library, but every calculation here is a small named function like daysBetween or contrastRatio sitting in the same file as the input listener that calls it.

Sources: MDN — CanvasRenderingContext2D · MDN — Intl.DateTimeFormat · MDN — Date · MDN — requestAnimationFrame · MDN — prefers-reduced-motion

Related reading: Number Stepper UI: 9 Interaction Patterns · Custom Color Picker: 9 JS Parts, No Library · CSS Chart Design: 9 SVG Graphs, No Library — the first shares this post's one-small-function-per-input shape on stepper controls rather than calculators, the second updates a visual from a live value the same way without any library either, and the third draws its nine graphs from typed or generated data with the same no-library, no-CDN rule this post follows.

Enter the archive password

The password is inside this article. You will find it as you read.