---
name: neumorphism
description: Use when you want "Soft, minimal, tactile" - Uses soft inner and outer shadows to make UI elements look pressed into the surface.
---

# Neumorphism

> **Category:** Glass / Depth  -  **Personality:** Soft, minimal, tactile
>
> **Best use:** Mobile UI, controls, settings screens
>
> **Ratings:** Professional 3/5 - Casual 4/5 - Exotic 3/5 - Visual impact 3/5 - Difficulty 3/5 - Performance cost 2/5

## What it is

Neumorphism ("new skeuomorphism") makes controls look **extruded from, or pressed into, a single soft surface**. Every element shares the exact same background colour as its parent; instead of borders or fills, two opposing `box-shadow`s — a light one to the top-left and a dark one to the bottom-right — fake a soft light source so the shape reads as raised. Swap those shadows to `inset` and the same element looks pressed in. It feels calm and tactile, which is why it suits thermostats, smart-home panels, music players, and settings screens. The demo is a "Hearth" room controller: a raised panel holding a radial temperature dial (a raised disc with a recessed groove track), a pressed-in segmented mode control, recessed-track toggle switches, and an inset slider.

## Dependencies / CDN

**None - pure CSS** for the effect itself (just paired `box-shadow`s). No JavaScript is required for the look; the demo's JS only adds interactivity (dial drag, steppers, toggles). The demo loads two display/body fonts — optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;500;600;700&family=Outfit:wght@300;400;500;600&display=swap" rel="stylesheet">
```

## HTML

The whole illusion is "same colour everywhere + shadows". A raised card, a pressed-in (inset) segmented control, a recessed-track switch, and the radial dial:

```html
<div class="nm-stage">           <!-- holds the palette + a mid-tone background -->
  <div class="nm-panel">         <!-- RAISED card extruded from the stage -->

    <!-- radial dial: raised disc + inset groove + SVG arc + raised centre well -->
    <div class="nm-dial" id="nmDial" data-mode="auto">
      <span class="nm-groove"></span>                         <!-- inset channel -->
      <svg class="nm-ring" viewBox="0 0 260 260">
        <circle class="nm-track" cx="130" cy="130" r="104" transform="rotate(135 130 130)"/>
        <circle class="nm-prog"  cx="130" cy="130" r="104" transform="rotate(135 130 130)" stroke-dasharray="204.2 9999"/>
      </svg>
      <span class="nm-knob" id="nmKnob"></span>
      <div class="nm-well"><span class="nm-temp"><span id="nmVal">21</span><i>&deg;</i></span></div>
    </div>

    <!-- segmented control: flat tray, the ACTIVE button is pressed in (inset) -->
    <div class="nm-modes">
      <button class="nm-mode" data-mode="cool" aria-pressed="false">Cool</button>
      <button class="nm-mode is-active" data-mode="auto" aria-pressed="true">Auto</button>
      <button class="nm-mode" data-mode="heat" aria-pressed="false">Heat</button>
    </div>

    <!-- switch: inset track holds a raised knob -->
    <button class="nm-toggle is-on" role="switch" aria-checked="true" aria-label="Eco mode">
      <span class="nm-knob2"></span>
    </button>

  </div>
</div>
```

## CSS

```css
/* 1) THE PALETTE — base, plus a LIGHTER and a DARKER tint derived from it.
      Everything uses --nm-bg; the shadows do all the shaping. */
.nm-stage{
  --nm-bg:#ece5db; --nm-light:#fefcf7; --nm-dark:#c8bfae;
  --nm-accent:#e98b5e; --nm-groove:#ddd5c7;
  background:var(--nm-bg);            /* the surface everything is carved from   */
}

/* 2) RAISED — the core recipe: light shadow top-left, dark shadow bottom-right */
.nm-panel{
  background:var(--nm-bg); border-radius:34px;
  box-shadow: 13px 13px 30px var(--nm-dark),
             -13px -13px 30px var(--nm-light);
}
.nm-step{                              /* a tappable raised button                */
  background:var(--nm-bg); border:none; border-radius:16px;
  box-shadow: 5px 5px 11px var(--nm-dark), -5px -5px 11px var(--nm-light);
  transition: box-shadow .14s ease, transform .14s ease;
}
/* 3) PRESSED — flip both shadows to `inset`. This is the "into the surface" look */
.nm-step:active{
  box-shadow: inset 4px 4px 9px var(--nm-dark),
              inset -4px -4px 9px var(--nm-light);
  transform: scale(.96);
}

/* 4) SWITCH — an inset track cradling a small raised knob */
.nm-toggle{
  position:relative; width:60px; height:33px; border:none; border-radius:999px;
  background:var(--nm-bg);
  box-shadow: inset 3px 3px 6px var(--nm-dark), inset -3px -3px 6px var(--nm-light);
}
.nm-knob2{
  position:absolute; top:4px; left:4px; width:25px; height:25px; border-radius:50%;
  background:var(--nm-bg); transition:left .22s ease, background .22s ease;
  box-shadow: 2px 2px 5px var(--nm-dark), -2px -2px 5px var(--nm-light);
}
.nm-toggle.is-on{ box-shadow: inset 3px 3px 7px #bf7048, inset -3px -3px 7px #ffac80; }
.nm-toggle.is-on .nm-knob2{ left:31px; background:linear-gradient(145deg,#f3996c,#e0794d); }

/* 5) SEGMENTED CONTROL — flat tray, active item pressed in */
.nm-modes{ background:var(--nm-bg); border-radius:18px;
  box-shadow: 5px 5px 11px var(--nm-dark), -5px -5px 11px var(--nm-light); }
.nm-mode.is-active{
  box-shadow: inset 4px 4px 8px var(--nm-dark), inset -4px -4px 8px var(--nm-light);
}

/* 6) RADIAL DIAL — raised disc, an inset groove, a raised centre well */
.nm-dial{ position:relative; aspect-ratio:1; border-radius:50%; background:var(--nm-bg);
  box-shadow: 11px 11px 24px var(--nm-dark), -11px -11px 24px var(--nm-light); }
.nm-groove{ position:absolute; inset:13px; border-radius:50%;
  box-shadow: inset 7px 7px 13px var(--nm-dark), inset -7px -7px 13px var(--nm-light); }
.nm-well{ position:absolute; inset:48px; border-radius:50%; background:var(--nm-bg); z-index:2;
  box-shadow: 5px 5px 13px var(--nm-dark), -5px -5px 13px var(--nm-light); }
.nm-track,.nm-prog{ fill:none; stroke-width:13; stroke-linecap:round; }
.nm-track{ stroke:var(--nm-groove); stroke-dasharray:490.09 163.36; }  /* 270° band, 90° gap at bottom */
.nm-prog{ stroke:var(--nm-accent); }

/* Keyboard focus must NOT rely on the soft shadow alone */
.nm-step:focus-visible,.nm-mode:focus-visible,.nm-toggle:focus-visible{
  outline:2px solid var(--nm-accent); outline-offset:3px;
}
```

## JavaScript

Not required for the effect. The demo wires up the dial (drag + `+/−` steppers), the switches, and the segmented control. The dial is an SVG ring whose progress is one dash starting bottom-left and sweeping 270° clockwise; the HTML knob is placed on that arc by trig.

```js
(function(){
  var dial=document.getElementById('nmDial'), knob=document.getElementById('nmKnob'),
      prog=dial.querySelector('.nm-prog'), valEl=document.getElementById('nmVal');
  var MIN=16, MAX=28, VIS=490.0885, temp=21;          // VIS = visible arc length (270° of r=104)

  function render(){
    var frac=(temp-MIN)/(MAX-MIN);
    var ang=(135+frac*270)*Math.PI/180;               // gauge starts bottom-left, sweeps 270°
    knob.style.left=(50+40*Math.cos(ang)).toFixed(2)+'%';   // 40% = ring radius as % of the box
    knob.style.top =(50+40*Math.sin(ang)).toFixed(2)+'%';
    prog.setAttribute('stroke-dasharray',(frac*VIS).toFixed(2)+' 9999');
    valEl.textContent=temp;
  }
  function setTemp(t){ temp=Math.max(MIN,Math.min(MAX,Math.round(t))); render(); }

  document.getElementById('nmUp').addEventListener('click',function(){setTemp(temp+1);});
  document.getElementById('nmDown').addEventListener('click',function(){setTemp(temp-1);});

  // Drag the dial face to scrub the temperature.
  var dragging=false;
  function fromPointer(e){
    var r=dial.getBoundingClientRect(), dx=e.clientX-(r.left+r.width/2), dy=e.clientY-(r.top+r.height/2);
    if(Math.hypot(dx,dy)<r.width*0.2) return;          // dead zone at the centre
    var a=Math.atan2(dy,dx)*180/Math.PI; if(a<0)a+=360;
    var frac = a>=135 ? (a-135)/270 : a<=45 ? (a+225)/270 : (a<90?1:0);  // snap across bottom gap
    setTemp(MIN+frac*(MAX-MIN));
  }
  dial.addEventListener('pointerdown',function(e){dragging=true;dial.classList.add('is-dragging');
    try{dial.setPointerCapture(e.pointerId);}catch(_){} fromPointer(e);});
  dial.addEventListener('pointermove',function(e){if(dragging)fromPointer(e);});
  dial.addEventListener('pointerup',function(){dragging=false;dial.classList.remove('is-dragging');});

  // Switches: toggle the class + keep aria-checked in sync.
  document.querySelectorAll('.nm-toggle').forEach(function(t){
    t.addEventListener('click',function(){
      t.setAttribute('aria-checked', t.classList.toggle('is-on') ? 'true':'false');
    });
  });

  render();
})();
```

## How it works

- **One surface, two shadows.** The element's `background` equals its parent's. A light shadow offset toward the (imagined) light source plus a dark shadow offset away from it fake a smooth bevel. Equal-but-opposite offsets read as *raised*.
- **`inset` = pressed.** The identical pair of shadows, prefixed with `inset`, carve the shape *into* the surface. This single swap is the whole "pressed into the surface" personality — and the only difference between the demo's resting buttons and their `:active` state.
- **Consistent light direction.** Every raised element here uses `+x +y` for the dark shadow and `−x −y` for the light one, so the whole UI looks lit from the same top-left source. Mixing directions breaks the illusion instantly.
- **The dial** layers the three states at once: a raised disc, an `inset` groove ring, and a raised centre well — proof that inner and outer shadows coexist on one colour. The coloured progress is an SVG `<circle>` rotated 135° with a `stroke-dasharray` whose visible run is 270° (gap parked at the bottom); JS sets the dash length and positions the knob with `cos/sin`.

## Customization

- **Palette is derived, not picked.** Choose one mid-tone `--nm-bg`, then set `--nm-light` a few % lighter and `--nm-dark` a few % darker. Keep all three in the same hue or the metal/clay feel turns muddy. Warm here (`#ece5db`); cool classic is `#e0e5ec / #ffffff / #a3b1c6`.
- **Softness & elevation:** larger blur + offset = higher float (panel uses `13px … 30px`; small buttons `5px … 11px`). Keep offset ≈ blur/2.
- **Pressed depth:** raise the `inset` blur for a deeper well; the toggle's "on" state tints the inset shadows with the accent for a lit-recess glow.
- **Roundness:** neumorphism wants generous `border-radius`; sharp corners fight the soft light.

## Accessibility & performance

- **Contrast is the #1 risk.** Same-colour surfaces give tiny element-vs-background contrast, and shadows are decorative — screen readers and low-vision users get nothing from them. Keep **text/icon** contrast ≥ 4.5:1 (the demo's `--nm-text:#4f4638` on `#ece5db` clears it) and never encode meaning in the shadow alone.
- **Never rely on raised/pressed for state.** The demo pairs every visual state with semantics: `role="switch"` + `aria-checked`, `aria-pressed` on the segmented buttons, and `aria-label`s on icon-only controls.
- **Visible focus.** Soft shadows make the default focus ring vanish, so a real `:focus-visible` outline is mandatory (provided above).
- **Motion.** `prefers-reduced-motion: reduce` disables the status-dot pulse; the JS uses pointer events, not animation loops, so it is reduced-motion-safe by default.
- **Performance** is low: `box-shadow` is cheap. The only caution is animating shadow *geometry* on many elements at once — prefer toggling between two pre-defined shadows (as here) and animating `transform` for press feedback.

## Gotchas

- **Element and parent MUST be the same colour.** On a contrasting background the "bevel" just looks like a drop shadow — the carved-from-one-surface effect disappears.
- **Avoid pure white or pure black bases.** You need headroom both lighter and darker than the base; `#fff`/`#000` can only throw one believable shadow.
- **Two shadows, always.** A single shadow looks like ordinary Material elevation; it's the *opposing pair* that sells the soft 3D.
- **No borders, no gradients-as-fills, minimal flat colour** on the shaped elements — they short-circuit the illusion. Use a subtle accent only for *content* (text, the dial arc, the "on" toggle).
- **Keep the light direction uniform** across the whole screen; one element lit from the opposite corner reads as a bug.
- **Don't over-use it.** Low contrast means dense, text-heavy or data-heavy UIs suffer; neumorphism shines on a *small set of large, friendly controls* — exactly the thermostat/settings use case.
