13 CSS Neumorphism & Soft UI Designs

Soft UI Calculator

A warm-cream desk calculator where every button is a fully extruded neumorphic circle with a CSS specular shine highlight at the top-left. Pressing triggers an inset shadow flip plus a ripple-ring animation expanding outward. The display is a deep-inset panel with a scanline grid overlay and smooth font-size scaling for long numbers. Operator keys (÷ × − +) are styled in burnt amber, function keys (AC ± %) in slate-gray with a slightly recessed background. The = key spans two columns as a pill-shaped amber gradient. Full keyboard input support — type numbers, operators, Enter, Escape directly. Space Mono numerals + Nunito keys. Best for calculator tools, finance widgets, conversion utilities.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

The code

<section class="nm-clc" aria-label="Soft UI calculator">
  <div class="card">

    <div class="display-wrap">
      <div class="display-expr" data-nm-clc-expr></div>
      <div class="display-result" data-nm-clc-result>0</div>
    </div>

    <div class="mem-row">
      <button type="button" class="mem-key" data-nm-clc-mem="MC">MC</button>
      <button type="button" class="mem-key" data-nm-clc-mem="MR">MR</button>
      <button type="button" class="mem-key" data-nm-clc-mem="M-">M−</button>
      <button type="button" class="mem-key" data-nm-clc-mem="M+">M+</button>
      <button type="button" class="mem-key" data-nm-clc-mem="MS">MS</button>
    </div>

    <div class="keypad">
      <button type="button" class="key fn" data-nm-clc-key="AC">AC</button>
      <button type="button" class="key fn" data-nm-clc-key="+/-">±</button>
      <button type="button" class="key fn" data-nm-clc-key="%">%</button>
      <button type="button" class="key op" data-nm-clc-key="/">÷</button>

      <button type="button" class="key" data-nm-clc-key="7">7</button>
      <button type="button" class="key" data-nm-clc-key="8">8</button>
      <button type="button" class="key" data-nm-clc-key="9">9</button>
      <button type="button" class="key op" data-nm-clc-key="*">×</button>

      <button type="button" class="key" data-nm-clc-key="4">4</button>
      <button type="button" class="key" data-nm-clc-key="5">5</button>
      <button type="button" class="key" data-nm-clc-key="6">6</button>
      <button type="button" class="key op" data-nm-clc-key="-">−</button>

      <button type="button" class="key" data-nm-clc-key="1">1</button>
      <button type="button" class="key" data-nm-clc-key="2">2</button>
      <button type="button" class="key" data-nm-clc-key="3">3</button>
      <button type="button" class="key op" data-nm-clc-key="+">+</button>

      <button type="button" class="key zero" data-nm-clc-key="0">0</button>
      <button type="button" class="key" data-nm-clc-key=".">.</button>
      <button type="button" class="key eq" data-nm-clc-key="=">=</button>
    </div>

  </div>
</section>
/* ─── 09 Soft UI Calculator — warm cream desk calculator ──────────── */
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Nunito:wght@400;600;700;800&display=swap');

.nm-clc {
  --nm-clc-bg:  #f0ebe0;
  --nm-clc-sd:  #d2cdc0;
  --nm-clc-sl:  #fffefc;
  --nm-clc-ib:  #e6e1d6;
  --nm-clc-op:  #c8783a;
  --nm-clc-op2: #a05828;
  --nm-clc-fn:  #5a6e7a;
  --nm-clc-txt: #6a6058;
  --nm-clc-txt2:#1a1510;

  --nm-clc-neu:       9px 9px 20px var(--nm-clc-sd), -9px -9px 20px var(--nm-clc-sl);
  --nm-clc-neu-in:    inset 7px 7px 16px var(--nm-clc-sd), inset -7px -7px 16px var(--nm-clc-sl);
  --nm-clc-neu-sm:    6px 6px 13px var(--nm-clc-sd), -6px -6px 13px var(--nm-clc-sl);
  --nm-clc-neu-sm-in: inset 5px 5px 11px var(--nm-clc-sd), inset -5px -5px 11px var(--nm-clc-sl);

  position: relative;
  width: 100%;
  min-height: 820px;
  background: var(--nm-clc-bg);
  font-family: 'Nunito', system-ui, sans-serif;
  color: var(--nm-clc-txt);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px 16px;
  overflow: hidden;
  box-sizing: border-box;
}

.nm-clc *,
.nm-clc *::before,
.nm-clc *::after { box-sizing: border-box; }

.nm-clc .card {
  position: relative;
  width: 100%;
  max-width: 400px;
  background: var(--nm-clc-bg);
  border-radius: 40px;
  padding: 36px 32px 32px;
  box-shadow: 20px 20px 50px var(--nm-clc-sd), -20px -20px 50px var(--nm-clc-sl);
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.nm-clc .card::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 40px;
  background: radial-gradient(ellipse at 20% 20%, rgba(255, 255, 255, 0.15) 0%, transparent 60%);
  pointer-events: none;
}

/* Display */
.nm-clc .display-wrap {
  background: var(--nm-clc-bg);
  border-radius: 24px;
  padding: 22px 22px 18px;
  box-shadow: var(--nm-clc-neu-in);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  min-height: 110px;
  justify-content: flex-end;
  position: relative;
  overflow: hidden;
}
.nm-clc .display-wrap::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg, transparent 0px, transparent 2px,
    rgba(0, 0, 0, 0.012) 2px, rgba(0, 0, 0, 0.012) 3px
  );
  border-radius: 24px;
  pointer-events: none;
}
.nm-clc .display-expr {
  font-family: 'Space Mono', ui-monospace, monospace;
  font-size: 13px;
  color: var(--nm-clc-txt);
  opacity: 0.65;
  min-height: 18px;
  letter-spacing: 0.5px;
  text-align: right;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  width: 100%;
}
.nm-clc .display-result {
  font-family: 'Space Mono', ui-monospace, monospace;
  font-size: 42px;
  font-weight: 700;
  color: var(--nm-clc-txt2);
  letter-spacing: -1.5px;
  line-height: 1;
  transition: transform 0.15s ease, color 0.15s ease;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: right;
  width: 100%;
}
.nm-clc .display-result.flash {
  color: var(--nm-clc-op);
  transform: scale(1.02);
}
@keyframes nm-clc-slide {
  from { transform: translateY(6px); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}
.nm-clc .display-result.fresh { animation: nm-clc-slide 0.18s ease-out; }

/* Memory row */
.nm-clc .mem-row {
  display: flex;
  gap: 10px;
  justify-content: center;
}
.nm-clc .mem-key {
  flex: 1;
  padding: 10px 0;
  background: var(--nm-clc-bg);
  border: none;
  cursor: pointer;
  border-radius: 14px;
  font-family: 'Space Mono', ui-monospace, monospace;
  font-size: 11px;
  font-weight: 700;
  color: var(--nm-clc-fn);
  box-shadow: var(--nm-clc-neu-sm);
  transition: all 0.12s;
  letter-spacing: 0.5px;
}
.nm-clc .mem-key:hover { box-shadow: 7px 7px 16px var(--nm-clc-sd), -7px -7px 16px var(--nm-clc-sl); }
.nm-clc .mem-key:active {
  box-shadow: var(--nm-clc-neu-sm-in);
  color: var(--nm-clc-op);
}

/* Keypad */
.nm-clc .keypad {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
.nm-clc .key {
  aspect-ratio: 1;
  background: var(--nm-clc-bg);
  border: none;
  cursor: pointer;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: 'Nunito', system-ui, sans-serif;
  font-size: 18px;
  font-weight: 700;
  color: var(--nm-clc-txt2);
  box-shadow: var(--nm-clc-neu-sm);
  transition: box-shadow 0.12s ease, transform 0.1s ease, color 0.15s;
  user-select: none;
  position: relative;
  outline: none;
}
.nm-clc .key::before {
  content: '';
  position: absolute;
  top: 12%;
  left: 15%;
  width: 30%;
  height: 25%;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.55);
  filter: blur(3px);
  pointer-events: none;
}
.nm-clc .key:hover {
  box-shadow: 8px 8px 18px var(--nm-clc-sd), -8px -8px 18px var(--nm-clc-sl);
  transform: translateY(-1px);
}
.nm-clc .key:active,
.nm-clc .key.pressed {
  box-shadow: var(--nm-clc-neu-sm-in);
  transform: scale(0.94);
  color: var(--nm-clc-op);
}

.nm-clc .key.fn {
  background: var(--nm-clc-ib);
  font-size: 14px;
  font-weight: 800;
  color: var(--nm-clc-fn);
  box-shadow: var(--nm-clc-neu-sm);
}
.nm-clc .key.fn:active,
.nm-clc .key.fn.pressed {
  box-shadow: var(--nm-clc-neu-sm-in);
  color: var(--nm-clc-op);
}

.nm-clc .key.op {
  background: var(--nm-clc-bg);
  color: var(--nm-clc-op);
  font-size: 22px;
}
.nm-clc .key.op::after {
  content: '';
  position: absolute;
  inset: 6px;
  border-radius: 50%;
  border: 1.5px solid rgba(200, 120, 58, 0.15);
  pointer-events: none;
}
.nm-clc .key.op:active,
.nm-clc .key.op.pressed {
  color: var(--nm-clc-op2);
  box-shadow: var(--nm-clc-neu-sm-in);
}

.nm-clc .key.eq {
  grid-column: span 2;
  border-radius: 32px;
  aspect-ratio: unset;
  padding: 0 24px;
  height: 64px;
  background: linear-gradient(145deg, var(--nm-clc-op), var(--nm-clc-op2));
  color: #fff8f0;
  font-size: 22px;
  box-shadow: 8px 8px 18px rgba(180, 90, 30, 0.35), -5px -5px 14px var(--nm-clc-sl);
}
.nm-clc .key.eq::before { display: none; }
.nm-clc .key.eq:active,
.nm-clc .key.eq.pressed {
  box-shadow: inset 5px 5px 14px rgba(0, 0, 0, 0.22);
  transform: scale(0.97);
  color: rgba(255, 248, 240, 0.85);
}

.nm-clc .key.zero {
  grid-column: span 2;
  border-radius: 32px;
  aspect-ratio: unset;
  padding: 0 24px;
  height: 64px;
  justify-content: flex-start;
}

@keyframes nm-clc-ripple {
  0% { transform: scale(0.95); opacity: 0.6; }
  100% { transform: scale(1.6); opacity: 0; }
}
.nm-clc .key.rippling::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid var(--nm-clc-op);
  animation: nm-clc-ripple 0.35s ease-out forwards;
  pointer-events: none;
}
.nm-clc .key.op.rippling::after {
  animation: nm-clc-ripple 0.35s ease-out forwards;
  border: 2px solid var(--nm-clc-op);
}

@media (prefers-reduced-motion: reduce) {
  .nm-clc .display-result,
  .nm-clc .key { transition: none; animation: none; }
}
(() => {
  const root = document.querySelector('.nm-clc');
  if (!root) return;
  let display = '0';
  let operand = null;
  let operator = null;
  let waitingForOperand = false;
  let memory = 0;
  const resultEl = root.querySelector('[data-nm-clc-result]');
  const exprEl = root.querySelector('[data-nm-clc-expr]');

  function updateDisplay(val, newExpr) {
    const len = String(val).length;
    resultEl.style.fontSize = len > 10 ? '26px' : len > 7 ? '34px' : '42px';
    resultEl.classList.remove('fresh');
    void resultEl.offsetWidth;
    resultEl.classList.add('fresh');
    resultEl.textContent = val;
    if (newExpr !== undefined) exprEl.textContent = newExpr;
  }

  function compute() {
    const cur = parseFloat(display);
    let result;
    switch (operator) {
      case '+': result = operand + cur; break;
      case '-': result = operand - cur; break;
      case '*': result = operand * cur; break;
      case '/': result = cur !== 0 ? operand / cur : 'Error'; break;
    }
    if (result === 'Error') {
      display = 'Error';
    } else {
      display = parseFloat(result.toFixed(10)).toString();
      if (display.length > 12) display = parseFloat(result.toExponential(4)).toString();
    }
    operand = parseFloat(display);
    waitingForOperand = true;
    updateDisplay(display);
  }

  function ripple(action) {
    const btn = root.querySelector('[data-nm-clc-key="' + action + '"]');
    if (!btn) return;
    btn.classList.add('pressed', 'rippling');
    setTimeout(() => btn.classList.remove('pressed', 'rippling'), 200);
  }

  function action(act) {
    ripple(act);
    if (/^\d$/.test(act)) {
      if (waitingForOperand) {
        display = act;
        waitingForOperand = false;
      } else {
        display = display === '0' ? act : display + act;
      }
      if (display.length > 12) return;
      updateDisplay(display, display);
      return;
    }
    switch (act) {
      case '.':
        if (!display.includes('.')) { display += '.'; updateDisplay(display); }
        break;
      case 'AC':
        display = '0'; operand = null; operator = null; waitingForOperand = false;
        resultEl.classList.add('flash');
        setTimeout(() => resultEl.classList.remove('flash'), 250);
        updateDisplay('0', '');
        break;
      case '+/-':
        display = String(-parseFloat(display));
        updateDisplay(display);
        break;
      case '%':
        display = String(parseFloat(display) / 100);
        updateDisplay(display);
        break;
      case '/': case '*': case '-': case '+':
        if (operator && !waitingForOperand) compute();
        operand = parseFloat(display);
        operator = act;
        waitingForOperand = true;
        updateDisplay(display, display + ' ' + (act === '*' ? '×' : act === '/' ? '÷' : act === '-' ? '−' : act));
        break;
      case '=':
        if (operator && !waitingForOperand) {
          compute();
          operator = null;
          resultEl.classList.add('flash');
          setTimeout(() => resultEl.classList.remove('flash'), 200);
          exprEl.textContent = '';
        }
        break;
    }
  }

  // Click handlers
  root.querySelectorAll('[data-nm-clc-key]').forEach(btn => {
    btn.addEventListener('click', () => action(btn.dataset.nmClcKey));
  });

  // Memory handlers
  root.querySelectorAll('[data-nm-clc-mem]').forEach(btn => {
    btn.addEventListener('click', () => {
      const op = btn.dataset.nmClcMem;
      switch (op) {
        case 'MC': memory = 0; break;
        case 'MR': display = String(memory); waitingForOperand = false; updateDisplay(display); break;
        case 'M+': memory += parseFloat(display); break;
        case 'M-': memory -= parseFloat(display); break;
        case 'MS': memory = parseFloat(display); break;
      }
    });
  });

  // Keyboard support
  document.addEventListener('keydown', e => {
    // Only if the focused element is inside our root, or no input is focused
    const ae = document.activeElement;
    const inField = ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.tagName === 'SELECT');
    if (inField && !root.contains(ae)) return;
    const map = {
      '0':'0','1':'1','2':'2','3':'3','4':'4','5':'5','6':'6','7':'7','8':'8','9':'9',
      '.':'.', 'Enter':'=', '=':'=',
      '+':'+', '-':'-', '*':'*', '/':'/',
      'Backspace':'AC', 'Escape':'AC', '%':'%'
    };
    if (map[e.key]) { e.preventDefault(); action(map[e.key]); }
  });
})();

Search CodeFronts

Loading…