Design Lounge
Install

Nº 334 of 520

Paper hydration tumbler

A paper-minimal water widget: an outlined tumbler whose liquid sloshes and rises on +250 ml, a 2 L goal, a dated log and a one-step undo.

Preview

Open full size
1280 × 800 · 10.0 KB · 191 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 "Paper hydration tumbler" (piece widget-hydration) 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

191 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

A water-intake widget for a fictional habit app called Sipwell, set like a page from a notebook: warm paper, a single hairline-bordered card, a light serif numeral, and one ink button. On the left an outlined tumbler holds the day’s water as a blue liquid with a two-layer wave. Tapping ”+ 250 ml” logs a glass: the liquid rises toward the new level, and the surface sloshes (a damped tilt plus a bigger wave that settles back to a gentle idle ripple). Undo removes the last glass with a smaller slosh. The detail worth copying is that the slosh is physical, not decorative: the tilt oscillates and decays, so the water looks like it was just poured.

Reference behaviour

  1. Initial state: 1,250 ml logged from five glasses (7:40, 9:15, 11:02, 13:30, 15:10). Liquid at 62.5% of the tumbler. Text: “of 2,000 ml · 750 ml to go”. Progress hairline at 62.5%.
  2. The surface ripples gently all the time (amplitude 1.6, phase +0.035/frame). A paler back wave runs 2px higher, out of phase, for depth.
  3. Click ”+ 250 ml”: a 250 ml entry is logged at the next simulated time (16:05, then +40 min each). The number updates immediately, the log’s newest row slides in from −6px, the progress hairline grows over 600ms.
  4. On the same click the liquid eases toward the new level (7% of the remaining distance per frame) and sloshes: wave amplitude jumps to 6.6 and decays back toward 1.6; a tilt of ±9px oscillates and decays with a ~70-frame time constant.
  5. At 2,000 ml: text becomes “Goal met · right on 2 L”, a cream check appears near the top of the liquid. Over 2 L: “Goal met · 250 ml over”; the liquid stays at the brim.
  6. ”+ 250 ml” disables at 3,500 ml.
  7. Click “Undo”: removes the newest entry (any entry, back to zero), steps the simulated clock back 40 min, slosh at 45% strength, liquid falls.
  8. Undo is disabled when nothing is logged; the log shows “Nothing logged yet · 0 ml”.
  9. The log shows the four newest entries, newest first. The header counts all glasses (“5 glasses”, “1 glass”).
  10. A polite live region says “Logged 250 ml. 1,500 of 2,000 ml.” or “Removed 250 ml. …”.

Structure

1280 × 800, paper #eeeae0
        ┌──────────────── 620px card, 1px #dbd4c5, radius 6 ────────────────┐
        │  padding 36 40 32 28                                               │
        │   ┌──────────┐ ─ 2 L      SIPWELL                       SAT 3 OCT  │
        │   │          │            1,250 ml       76px Spectral 300         │
        │   │          │ ─ 1500     of 2,000 ml · 750 ml to go               │
        │   │~~~~~~~~~~│            ━━━━━━━━━━━━━━──────────  2px bar        │
        │   │██████████│ ─ 1000     [   +  250 ml      ] [ ↶ Undo ]  48px    │
        │   │██████████│ ─ 500      TODAY                        5 GLASSES   │
        │   └──────────┘            15:10                           250 ml   │
        │   200px column            13:30                           250 ml   │
        │   svg 200×270             11:02                           250 ml   │
        │                           9:15                            250 ml   │
        └────────────────────────────────────────────────────────────────────┘
  • main.card grid 200px 1fr, gap 36px, labelled by the amount.
  • .glass contains an svg viewBox="0 0 200 270" (aria-hidden): a clipPath of the tumbler interior, a clipped group with the back wave, front wave and a 3px white reflection line, then the outline, rim, tick group and the goal check.
  • Tumbler outline: top 20→160 at y=10, tapering to 36→144 at y≈254, 8px rounded base to y=262. Interior clip is inset ~4px.
  • Ticks sit outside the right wall at 500 / 1000 / 1500 / 2 L, each a 6px line plus a 10px label.
  • .side: p.eyebrow (brand left, date right), p.amount, p.of, .bar, .actions (two buttons), p.log-h, ul.log.

Tokens

:root {
  --bg: #eeeae0;         /* paper */
  --card: #f8f6f0;       /* sheet */
  --line: #dbd4c5;       /* hairlines */
  --line-2: #c9c0ae;     /* undo border */
  --ink: #22262a;        /* text, outline, primary button */
  --ink-2: #4f5459;
  --ink-3: #73787c;
  --water: #2f5fd0;      /* the one accent: liquid, progress, focus */
  --water-back: #a9c0ee; /* back wave */
  --serif: "Spectral", Georgia, serif;
  --sans: "Albert Sans", system-ui, sans-serif;
  --r: 6px; --r-btn: 4px;
  --space: 4px 8px 12px 18px 22px 28px 36px;
  --shadow-card: 0 1px 0 #fff inset, 0 24px 40px -32px rgba(34, 38, 42, .35);
  --ease: cubic-bezier(.2, .7, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --t-micro: 140ms; --t-row: 320ms; --t-bar: 600ms;
  --goal-ml: 2000; --step-ml: 250; --max-ml: 3500;
}

Typography

RoleFamilySizeWeightLine-heightTrackingNotes
AmountSpectral76px300.9−0.03emlining, tabular nums
Unit “ml”Spectral italic24px30010--ink-2
Goal lineAlbert Sans15px400, strong 6001.50—
ButtonsAlbert Sans15px60010—
Eyebrow / log headerAlbert Sans11px60010.16emuppercase, --ink-3
Log rowsAlbert Sans14px4001.50tabular nums
Tick labelsAlbert Sans10px50010.04em--ink-3

The serif is used only for the amount and unit. Everything else is the grotesk.

Motion

ThingTriggerPropertyFrom → toDurationEasingReduced motion
Idle ripplealways (rAF)wave path damplitude 1.6, phase +0.035/framecontinuoussineflat surface, no loop
Leveladd / undosurface yold → new~0.6s (7%/frame approach)exponential approachjumps
Slosh waveadd (1.0), undo (0.45)amplitude1.6 + 5×s → 1.6~1.5s (2.5%/frame decay)exponentialnone
Slosh tiltadd / undolinear tilt across width9×s × sin(0.16·t) × e^(−t/70)~1.2sdamped sinenone
Progress baradd / undowidthold% → new%600msexpo outinstant
New log rowaddopacity, translateY0, −6px → 1, 0320msexpo outnone
Goal checkreach 2 Lopacity0 → 1320msstandardinstant

The idle ripple is calm enough to sit in a grid of other pieces: under 2px of travel.

States

  • Primary hover: ink deepens to #000. Active: scale .98.
  • Undo hover: border turns --ink. Disabled: --ink-3 text, --line border, not-allowed cursor.
  • Add disabled (≥3,500 ml): fill --line-2.
  • Focus-visible on both buttons: 2px --water outline, 3px offset.
  • Goal met: .met on the glass shows the check; goal line text changes.
  • Empty: undo disabled, log placeholder row, liquid at the base.

Accessibility

  • Buttons are real buttons with visible text (“250 ml” with a plus icon, “Undo” with an arrow icon). Icons are aria-hidden.
  • The tumbler svg is decorative (aria-hidden); the amount and goal line carry the same information as text.
  • Live region (polite) announces each add/undo with the running total.
  • Undo uses the disabled attribute, not just styling.
  • Contrast: ink on #f8f6f0 ≈ 14:1; #73787c ≈ 4.5:1 for small caps labels; white button text on ink ≈ 14:1.
  • Buttons are 48px tall; Undo is at least 96px wide.

Responsive rules

  • ≥1024: card 620px.
  • 768: same.
  • <560: single column; tumbler 150px wide centred on top; numeral 60px; buttons and log full width; body gets 24px vertical padding.
  • At 375 wide there is no horizontal overflow; the card may scroll vertically in short frames.
  • In a home-screen small widget, keep only the tumbler, amount and the add button; move the log to the app.

Acceptance checklist

Always

  • Liquid is clipped to the vessel’s interior and drawn as two out-of-phase waves.
  • Adding sloshes (bigger wave + damped tilt) and the level eases up; undo sloshes less.
  • Number, goal line, progress hairline and log update in the same click.
  • Undo removes exactly the last entry and is disabled when the log is empty.
  • The add button has a ceiling.
  • Goal-met state changes the copy and shows a mark on the vessel; overflow says how much over.
  • One accent (water blue) used for liquid, progress and focus only.
  • Reduced motion: flat surface, level jumps, no row animation.
  • Live region announces add/undo.

This demo

  • Brand “Sipwell”, date “Sat 3 Oct”.
  • Starts at 1,250 ml from five 250 ml glasses at 7:40, 9:15, 11:02, 13:30, 15:10.
  • Goal 2,000 ml, step 250 ml, ceiling 3,500 ml.
  • First new glass is logged at 16:05, then every +40 min.
  • Ticks at 500, 1000, 1500 and “2 L”.

Implementation notes

The wave path. Rebuild one path per frame from a sine plus a linear tilt about the vessel’s centre, then close it below the base. Clip with the interior shape so the path can be wider than the glass.

function surface(y0, a, ph, tilt) {
  let d = `M0 ${BOTTOM + 10}`;
  for (let x = 0; x <= 200; x += 8) {
    const y = y0 + a * Math.sin(x * 0.045 + ph) + tilt * (x - 90) / 90;
    d += ` L${x} ${y.toFixed(2)}`;
  }
  return d + ` L200 ${BOTTOM + 10} Z`;
}
function frame() {
  level += (target - level) * 0.07;           // ease toward new level
  amp += (1.6 - amp) * 0.025;                 // slosh settles to idle
  phase += 0.035 + amp * 0.004;               // agitated water moves faster
  t++;
  const tilt = tiltA * Math.sin(t * 0.16) * Math.exp(-t / 70);
  front.setAttribute('d', surface(level, amp, phase, tilt));
  back.setAttribute('d', surface(level - 2, amp * .8, phase + 2.2, -tilt * .7));
  requestAnimationFrame(frame);
}

Slosh on input. slosh(s) just kicks the state: amp = 1.6 + 5*s; tiltA = 9*s; t = 0;. Add uses 1, undo 0.45. Pause the loop on visibilitychange.

Level mapping. levelFor(ml) = BOTTOM − min(1, ml / GOAL) × (BOTTOM − TOP) with TOP 22 and BOTTOM 256 in svg units. The vessel is the goal; going over doesn’t overflow the drawing.

Common mistakes:

  • A rectangle progress bar dressed as a glass. The tapered outline and the clip are the point.
  • CSS keyframe waves that loop the same way after a tap; the slosh must react to the tap.
  • Glassmorphism, gradients in the water, bubbles. Paper-minimal means one flat blue and a white reflection line.
  • Undo that only hides the row but keeps the total.
  • Using the current wall clock for log times without sorting; simulate or sort.
  • Rounding corners to 16px+; this card is 6px, buttons 4px.

Details

Palette
Type
Spectral · Albert Sans
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Tailwind CSS docs

A control. It studies this site for quiet, not for a show.

All sources

On the shelves