Design Lounge
Install

Nº 459 of 520

Tomato pomodoro timer widget

A focus timer card where the countdown ring is the tomato: start, pause, skip, reset, focus and break modes, and four session dots.

Preview

Open full size
1280 × 800 · 11.3 KB · 202 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 "Tomato pomodoro timer widget" (piece widget-pomodoro) 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

198 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 pomodoro widget for a desktop focus app or a dashboard corner. The card is cream with a 2.5px brown outline and a hard 8px offset shadow, like a sticker. The countdown ring is drawn as the tomato itself: a thick red ring with a green star-shaped calyx and stem sitting on top at 12 o’clock. The ring empties clockwise as time runs down. Three modes (Focus 25, Short break 5, Long break 15) recolour the ring: tomato red for focus, leaf green for breaks. Four session dots under the controls show progress through a set. The detail worth copying is that the ring and the mascot are the same object, so the whimsy costs no extra space.

Reference behaviour

  1. The first frame is mid-session and running: Focus mode, 17:24 remaining, label “Focus · 2 of 4”, the first dot filled, the second dot ringed as current, “Next: short break · 5 min”, “Today: 50 min focused”.
  2. While running, the time drops each second and the ring’s arc shrinks smoothly (1000ms linear per second). The calyx sways ±5° over 3.2s, alternating.
  3. The big centre button toggles Start / Pause. Its icon swaps between a pause glyph and a play triangle; its label swaps between “Pause” and “Start”.
  4. Skip ends the current block immediately. From Focus it fills the current session dot (dot pops to 1.3× and back), then goes to Short break, or to Long break after session 4. From a break it goes to Focus and advances the session (after Long break the set restarts at session 1 with no dots filled). Skip keeps the running state.
  5. Reset sets the current mode back to its full length and pauses.
  6. Clicking a mode chip switches mode, sets its full length and pauses. The face does a 600ms pop (scale .9 → 1).
  7. When the countdown reaches 0:00 it advances automatically exactly like Skip, and keeps running.
  8. Every full minute of running Focus adds one to “Today: N min focused”.
  9. Keyboard anywhere on the page (not inside a button): Space toggles start/pause, S skips, R resets.
  10. The tab title shows “17:24 · Focus” and updates every second.
  11. Reduced motion: no sway, no ring tween (the arc jumps each second), no pops, no colour transitions.

Structure

1280 × 800, card centred, width min(420px, 100vw − 32px)
┌──────────── card, radius 36px, 2.5px ink, shadow 8px 8px 0 ink ───────────┐
│ Pommo (18px display)                          Today: 50 min focused       │
│ ┌───────────── modes pill, 2px ink, padding 4px ─────────────┐            │
│ │ [ Focus ]      Short break        Long break               │ 40px tall  │
│ └────────────────────────────────────────────────────────────┘            │
│                        ✶ calyx + stem                                     │
│                   ╭──── ring 272px ────╮                                  │
│                   │       17:23        │  46px display                    │
│                   │   FOCUS · 2 OF 4   │  14px caps                       │
│                   ╰────────────────────╯                                  │
│            (reset 52px)   (play 84px)   (skip 52px)   gap 22px            │
│                        ● ◎ ○ ○                                            │
│                 Next: short break · 5 min                                 │
│        [Space] start or pause · [S] skip · [R] reset                      │
└───────────────────────────────────────────────────────────────────────────┘
padding 24px 28px 28px
  • The card is a section labelled by the wordmark.
  • The modes are a div role="group" of three buttons with aria-pressed.
  • The ring is one inline SVG (viewBox="0 0 300 300", ring centre 150,158, r 118) with: an ink circle (stroke 25) for the outline, the track circle (stroke 20), the progress circle (stroke 20, round caps, rotated −90°), and the calyx group.
  • The time is a div role="timer" overlaid in the ring’s centre.
  • Controls are three buttons. Session dots are spans inside a div role="img" with a descriptive aria-label.

Tokens

:root {
  --bg: #ffe9d6;          /* page, with 1.5px tomato dots on a 28px grid */
  --card: #fff8ef;        /* card surface */
  --ink: #3a1610;         /* outline, shadow, text */
  --ink-2: #7a4a3c;       /* muted text */
  --line: #f0d3bf;
  --tomato: #e8442e;      /* focus accent */
  --tomato-soft: #f8d0c3; /* focus track */
  --leaf: #2f7d4f;        /* break accent, calyx */
  --leaf-soft: #cfe6d4;   /* break track */
  --display: "Rammetto One", Georgia, serif;
  --sans: "Nunito", system-ui, sans-serif;
  --pop: cubic-bezier(.34,1.56,.64,1);
  --ease: cubic-bezier(.2,.7,.2,1);
}
.widget[data-mode="focus"] { --accent: var(--tomato); --accent-soft: var(--tomato-soft); }
.widget[data-mode="short"], .widget[data-mode="long"] { --accent: var(--leaf); --accent-soft: var(--leaf-soft); }

Spacing: 4, 6, 10, 16, 18, 22, 24, 26, 28, 30. Radii: card 36px, pills 999px, round buttons 50%, kbd 5px.

Typography

RoleFamilySize / lineWeightTrackingCase
CountdownRammetto One46px / 1400−0.02emtabular
WordmarkRammetto One18px400−0.01emTitle, tomato
Mode label in ringNunito14px8000.12emUpper, accent
Mode chipsNunito14px8000Sentence
Today stat, NextNunito13–14px7000Sentence
Key hintsNunito12px, kbd 11px 800—0—

Rammetto One is very wide. At 46px “17:23” is about 170px, which fits inside the 200px inner ring at 272px. Do not go above 48px.

Motion

ThingTriggerPropertyFrom → toDurationEasingReduced motion
Progress arceach secondstroke-dashoffsetC·(1 − r/L) → next second1000mslinear (time-driven)jump
Arc on pause/modestate changestroke-dashoffsetset instantly0—same
Ring colourmode changestrokered ↔ green300ms—easeinstant
Calyx swayrunningrotate−5° ↔ 5°3.2s alternate, infinite—easenone
Face popmode changescale.9 → 1600ms—popnone
Dot fillsession donescale + fill1.3 → 1300ms—popinstant
Round buttons:activetranslateY, shadow0, 4px → 4px, 0120ms—easeinstant
Play button:activetranslateY0 → 6px120ms—easeinstant

Linear is correct for the arc because it represents time, not a UI move.

States

  • Running: pause icon, label “Pause”, calyx sways, current dot has a 2px tomato ring with a 3px card gap.
  • Paused: play icon, label “Start”, no sway, arc frozen at the exact remaining time.
  • Focus mode: tomato accent; Break modes: leaf accent on ring, play button, active chip and label.
  • Mode chip: rest --ink-2, hover --ink, pressed filled with --ink, card-coloured text and a 3px inset bottom bar in --accent (box-shadow: inset 0 -3px 0 var(--accent)).
  • Round buttons: hover background --bg; active pressed down onto their shadow.
  • Focus-visible: 3px --ink outline, offset 3px, on every button.
  • Session dots: empty (ink outline), done (tomato fill), current (outer ring, only in Focus).

Accessibility

  • The play button’s aria-label flips between “Start” and “Pause”. Reset and skip are labelled “Reset timer” and “Skip to next”.
  • Mode chips use aria-pressed; exactly one is true.
  • The time is role="timer" with aria-live="off" so it is readable but not announced every second.
  • “Next: …” is aria-live="polite" and changes only when the block changes.
  • The session dots have role="img" and aria-label="Session 2 of 4, 1 completed".
  • Shortcuts only fire when focus is not on a button, so Space on a focused button still activates that button once.
  • Hit targets: chips 40px tall, round buttons 52px, play 84px. Contrast: ink on card ≈ 14:1; the pressed chip is card text on ink (≈ 14:1). Never put 14px light text on the tomato red; it is only about 3.4:1. The play button carries an icon only, which needs 3:1.

Responsive rules

  • ≥1280 to 768: card 420px, centred on the dotted page.
  • <640: card width 100vw − 32px; the ring stays 272px but has max-width: 100%. At 375 nothing overflows; chip labels still fit.
  • Embedded in a sidebar 320px wide: keep the ring ≥ 220px and drop the key-hint line.

Acceptance checklist

Always

  • The countdown ring is also the mascot: a calyx and stem sit on the ring at 12 o’clock.
  • Ring empties clockwise from 12 o’clock with round caps.
  • Remaining time is computed from an end timestamp, not by decrementing a counter, so background tabs stay accurate.
  • Focus and break modes recolour the ring, the play button and the active chip’s bottom bar.
  • Skip and auto-complete fill a session dot; after session 4 Focus goes to Long break.
  • Reset and mode change pause the timer.
  • Space / S / R work when focus is not on a button.
  • Every button has a visible 3px focus ring and a hit target ≥ 40px.
  • Reduced motion removes sway, pops and the arc tween.

This demo

  • Brand “Pommo”; first frame 17:24 Focus, session 2 of 4, one dot done, running.
  • Lengths 25 / 5 / 15 minutes.
  • Card #fff8ef, outline #3a1610, shadow 8px 8px 0.
  • Ring r 118 in a 300 viewBox, stroke 20 over a 25 ink outline.

Implementation notes

Drift-free countdown. Store the end time and derive the remaining seconds; tick at 250ms so the display never skips a second:

function setRunning(r) {
  running = r; clearInterval(timer);
  if (r) { endAt = Date.now() + remaining * 1000; timer = setInterval(tick, 250); }
}
function tick() {
  const left = Math.max(0, Math.ceil((endAt - Date.now()) / 1000));
  if (left !== remaining) { remaining = left; paint(); }
  if (left === 0) advance(true);
}

Leading the arc by one second. If you set the dashoffset to the current remaining value with a 1s transition, the ring is always a second behind the digits. Set it to the value it will have at the next tick:

const C = 2 * Math.PI * 118; // 741.4
const shown = running ? Math.max(0, remaining - 1) : remaining;
prog.style.strokeDashoffset = (C * (1 - shown / LEN[mode])).toFixed(1);

When pausing or switching mode, add a class that removes the transition for that frame so the arc doesn’t animate backwards.

Calyx shape. A squashed five-point star centred on the ring top, scaled 1.35:

<g transform="translate(150 38) scale(1.35) translate(-150 -38)">
  <path fill="none" stroke-width="3.5" d="M150 36C150 28 152 20 158 13"/>
  <path d="M150 22.6L155.3 34L176.6 33.2L158.6 39.5L166.5 50.5L150 43L133.5 50.5L141.4 39.5L123.4 33.2L144.7 34Z"/>
</g>

Common mistakes:

  • A thin 4px ring; the tomato reading needs a fat stroke and the ink outline.
  • Decrementing remaining-- in a 1000ms interval; it drifts and freezes in background tabs.
  • Global Space handler that also fires when a button has focus, toggling twice.
  • Using the display face for chip labels; Rammetto at 14px is mushy.
  • Soft blurred shadows; the sticker look is a hard offset shadow with zero blur.

Details

Palette
Type
Rammetto One · Nunito
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Stripe

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

All sources

On the shelves