Design Lounge
Install

Nº 416 of 520

Soft inset clay buttons

Neumorphic buttons, icon toggles, switches and a segmented control on a clay surface, with AA text, a hairline edge and colour as the second state signal.

Preview

Open full size
1280 × 800 · 14.9 KB · 230 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Soft inset clay buttons" (piece button-inset-soft) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

193 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

The control panel of a ceramics kiln controller, “Kilnhouse”, built in soft UI. Everything is the same terracotta-clay colour; shape comes only from a paired light and dark shadow. Buttons are raised pillows that sink into the clay when pressed. Toggles stay sunk while on. A switch and a three-way segmented control sit in carved wells. The piece is worth copying because it fixes the two things that make neumorphism fail: every control also has a 1px hairline edge so it reads without the shadows, and every “on” state also turns its label or icon ember-red, so state never relies on depth alone. All text clears 4.5:1.

Reference behaviour

  1. First frame: a raised clay stage, radius 40px. Top left “Kilnhouse” in Gloock 34px and “Kiln 2 · Stoneware · target 1,222°C”. Top right a carved status pill with a dot: “Idle · 24°C”.
  2. Left column “Buttons · L / M / M / S”: Start firing (large, ember text, small ember dot), Hold temp, Vent, Unload (small, disabled, dashed outline).
  3. Below that “Icon toggles · 56px”: three round toggles. Chamber light starts on (sunk, ember icon); Exhaust fan and Lock door start off.
  4. Right column “Toggles”: Auto vent switch (on, ember knob slid right, “On”), Slow cool switch (off, “Off”). Below, “Programme”: Bisque / Glaze / Raku segmented control with Glaze selected.
  5. Hover on a raised button: its shadow grows from 6px to 9px offset, so it lifts towards you.
  6. Press: the button flips to an inset shadow and scales to 0.985 in 90ms. Release returns over 200ms.
  7. Start firing toggles: it stays sunk with aria-pressed="true", its label becomes “Stop firing”, and the status reads “Firing · ramp 150°C/h” with the dot turning ember with a 3px halo. Pressing again restores “Idle · 24°C”.
  8. Hold temp sets the status to “Holding at 1,040°C” and Vent to “Vent opened for 30 s”; after 2.4s the status returns to the firing or idle line.
  9. Icon toggles flip aria-pressed; on = inset + ember icon.
  10. Switches flip aria-checked; the knob slides 30px in 280ms, becomes ember, and the track tints ember at 14%. The trailing word flips On/Off.
  11. Segmented control: click or arrow keys move selection; a raised thumb slides under the chosen label in 320ms, and the label turns ember 600.
  12. A “States” sheet underneath shows Primary, Raised, Icon toggle and Switch at Default, Hover, Pressed, Disabled, Focus.
  13. Reduced motion: all transitions removed, no press scale.

Structure

1280 × 800, stage width min(1060px, 100%), padding 34/40/32, radius 40
┌───────────────────────────────────────────────────────────────────────────┐
│ Kilnhouse (Gloock 34)                               (● Idle · 24°C) well  │
│ Kiln 2 · Stoneware · target 1,222°C                                       │
│                                                                           │
│ BUTTONS · L / M / M / S              1.5fr  │ TOGGLES                1fr  │
│ (● Start firing) (Hold temp) (Vent) [Unload]│ (◯●) Auto vent  On          │
│   60px            52px       52px    42px   │ (●◯) Slow cool  Off         │
│ ICON TOGGLES · 56PX                         │ PROGRAMME                   │
│ (bulb) (fan) (lock)                         │ [ Bisque |(Glaze)| Raku ]   │
│───────────────────────────── 1px hairline ────────────────────────────────│
│ STATES       DEFAULT  HOVER  PRESSED  DISABLED  FOCUS                     │
│ Primary      (Fire) …                                                     │
│ Raised       (Vent) …                                                     │
│ Icon toggle  (lock) …                                                     │
│ Switch       (◯●) …                                                       │
└───────────────────────────────────────────────────────────────────────────┘
  • Stage is main labelled by the h1.
  • Status is a p aria-live="polite".
  • Buttons are button type="button"; Start firing gains aria-pressed after first press.
  • Icon toggles are button with aria-label and aria-pressed.
  • Switches are button role="switch" aria-checked, the visible text is the name; the On/Off word is aria-hidden.
  • Segmented control is div role="radiogroup" labelled by its caption, with three button role="radio" using roving tabindex; the thumb is an aria-hidden span.
  • The states grid is inert and aria-hidden, with a visually hidden sentence before it.

Tokens

:root {
  --clay: #e3cbb8;      /* surface: page, stage and every control */
  --hi: #f7e8db;        /* light shadow (top-left) */
  --lo: #b8957d;        /* dark shadow (bottom-right) */
  --ink: #3a261b;       /* text ~10:1 */
  --ink-2: #6b4a38;     /* captions ~5:1 */
  --ember: #8f3219;     /* the one accent: on-state text, knob, dot ~5:1 */
  --line: rgba(90,55,35,.14);  /* hairline on every raised control */
  --off-ink: #a08573;   /* disabled text */
  --serif: "Gloock", Georgia, serif;
  --sans: "Lexend", system-ui, sans-serif;
  --ease: cubic-bezier(.2,.7,.2,1);
  --out: cubic-bezier(.16,1,.3,1);
  --raise: 6px 6px 14px var(--lo), -6px -6px 14px var(--hi);
  --raise-hi: 9px 9px 20px var(--lo), -9px -9px 20px var(--hi);
  --sink: inset 4px 4px 9px var(--lo), inset -4px -4px 9px var(--hi);
  --well: inset 3px 3px 7px var(--lo), inset -3px -3px 7px var(--hi);
}

Stage shadow: 14px 14px 34px var(--lo), -14px -14px 34px var(--hi) plus 1px solid rgba(255,255,255,.35).

Sizes: L 60px tall / 32px padding / radius 20; M 52 / 26 / 18; S 42 / 18 / 14; icon 56px circle. Switch track 64×34, knob 26. Segmented: 5px well padding, items 42px tall, thumb radius 14, well radius 18.

Spacing: 4, 6, 10, 14, 16, 22, 26, 32, 40.

Typography

RoleFamilySizeWeightTrackingCase
WordmarkGloock34px / 1400−0.01emTitle
Meta, statusLexend13px400 / 5000Sentence
Button labelsLexend14 / 15 / 16px500 (primary and on 600)0Sentence
Captions, column headsLexend11px / 10px5000.12emUpper
On/Off wordLexend12px4000Title

Gloock appears once. Never set button labels in it.

Motion

ThingTriggerPropertyFrom → toDurationEasingReduced motion
Lift:hoverbox-shadowraise → raise-hi200ms—easenone
Press:activebox-shadow, scaleraise → sink, 1 → .98590ms—easeshadow swap only
Toggle onaria-pressedbox-shadow, colourraise → sink, ink → ember200ms—easeinstant
Switch knobaria-checkedtranslateX, fill0 → 30px, clay → ember280ms—outinstant
Segment thumbselectiontranslateXn × 100%320ms—outinstant
Status dotfiringfillink-2 → ember240ms—easeinstant

States

  • Default raised: --raise + 1px --line border.
  • Hover: --raise-hi.
  • Pressed (momentary): --sink, scale .985.
  • On / selected: --sink and ember text or icon at 600. This is the non-depth signal.
  • Disabled: no shadow, 1px dashed rgba(90,55,35,.28), --off-ink text, cursor: not-allowed. Disabled switch knob #d9bfab, flat.
  • Focus-visible: 2px --ember outline, offset 4px. Switch draws it on the track, not the whole label row. Segments draw it inset (offset −3px) so it stays inside the well.

Accessibility

  • Contrast: ink on clay ~10:1, --ink-2 ~5:1, --ember ~5:1. The disabled --off-ink is intentionally below 4.5 (disabled controls are exempt) but the dashed outline still marks the shape.
  • Shadows alone are under 3:1. The hairline, the dashed disabled outline and the ember on-state carry the meaning.
  • Switches: Space and Enter toggle; name comes from the visible label, state from aria-checked.
  • Segmented: Tab enters on the selected radio; Left/Up and Right/Down move and select, wrapping around.
  • Icon toggles: fixed labels (“Chamber light”, “Exhaust fan”, “Lock door”) with aria-pressed.
  • Status is the only live region.
  • Hit targets ≥ 42px; switch row ≥ 44px.

Responsive rules

  • ≥ 1024: two columns, 1.5fr / 1fr, gap 32.
  • < 860: one column; stage padding 26/20, radius 28; the segmented control goes full width; the states grid drops its label column (labels span above each row) and shrinks controls to 40px.
  • At 375 the four text buttons wrap onto two lines; nothing overflows.
  • Do not use this on a white or dark page. Soft UI only works when the page and the controls are the same colour.

Acceptance checklist

Always

  • Page, stage and controls share one surface colour.
  • Raised = two outer shadows (light top-left, dark bottom-right); pressed/on = the same pair inset.
  • Every raised control has a 1px hairline edge.
  • On states change colour as well as depth.
  • Disabled controls are flat with a dashed outline.
  • All body and label text ≥ 4.5:1.
  • Switches use role="switch"; segments use a radiogroup with arrow keys.
  • Focus ring 2px accent, offset 4px, visible on every control.
  • A states sheet shows default, hover, pressed, disabled and focus, and is inert.
  • Reduced motion removes all transitions.

This demo

  • Brand “Kilnhouse”, status “Idle · 24°C”, toggling Start firing gives “Stop firing” and “Firing · ramp 150°C/h”.
  • Chamber light and Auto vent start on; Glaze starts selected.
  • Clay #e3cbb8, shadows #f7e8db / #b8957d, ember #8f3219.
  • Hold temp and Vent show their message for 2.4s.

Implementation notes

Shadows as tokens. Define the four shadow recipes once; every control picks one:

.soft { background: var(--clay); border: 1px solid var(--line); box-shadow: var(--raise);
  transition: box-shadow 200ms var(--ease), transform 200ms var(--ease), color 200ms var(--ease); }
.soft:hover { box-shadow: var(--raise-hi); }
.soft:active { box-shadow: var(--sink); transform: scale(.985); transition-duration: 90ms; }
.soft[aria-pressed="true"] { box-shadow: var(--sink); color: var(--ember); font-weight: 600; }
.soft:disabled { box-shadow: none; border: 1px dashed rgba(90,55,35,.28); color: var(--off-ink); }

Segment thumb. One absolutely positioned thumb, a third of the inner width, moved by percent of its own width:

function pick(i, focus) {
  radios.forEach((r, j) => { r.setAttribute('aria-checked', j === i); r.tabIndex = j === i ? 0 : -1; });
  thumb.style.transform = `translateX(${i * 100}%)`;
  if (focus) radios[i].focus();
}

Thumb CSS: top:5px; left:5px; width:calc((100% - 10px)/3); height:calc(100% - 10px).

Common mistakes:

  • Pale grey text on the clay because “soft”. It fails contrast; use --ink.
  • Depth as the only state signal. Users with low vision cannot see a 4px inset.
  • Shadows on a page colour that differs from the control colour; it turns into a cheap bevel.
  • Pure white highlight. The light shadow is a warm #f7e8db, not #fff.
  • Blurring the shadows past ~20px on small controls; they turn to fog.

Details

Palette
Type
Gloock · Lexend
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Wickret

Let the big words move. Letters can break apart and come back as new words.

All sources