Design Lounge
Install

Nº 057 of 520

PWA update toast

A 'New version available' toast rises above the bottom nav with a Reload action and a 10s countdown ring; Reload sweeps a lime panel over the screen showing v2.4.1, then reveals the updated app.

Preview

Open full size
390 × 844 · 10.6 KB · 117 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 "PWA update toast" (piece pwa-update-toast) 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

218 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 “new version available” toast for “Kestrel”, a task-list PWA, shown when a service worker has installed a fresh build and is waiting to activate. A light toast rises from just above the bottom nav: lime download tile, “New version available / v2.4.1 · fixes offline sync”, a dark “Reload” button and a dismiss × wrapped in a 36px countdown ring that empties over 10s before auto-dismissing. Reload hides the toast and sweeps a full-screen lime panel up from the bottom, holds “v2.4.1” for 900ms, then sweeps it off the top to reveal the same page with its version pill updated. The detail worth copying is the countdown ring: an SVG circle’s stroke-dashoffset animated once over exactly the auto-dismiss duration, restarted by reflow whenever the toast is re-shown.

Reference behaviour

  1. Initial state: dark task list (7 tasks, 2 done) with a “v2.4.0” outline pill in the header; bottom nav with “Today” current in lime; the toast is already visible at bottom:112px and its ring is counting down from full.
  2. The ring (r=16, circumference 100.5) animates stroke-dashoffset 0 → 100.5 over 10s linear. At 10s the toast drops 24px and fades over 240ms cubic-bezier(.4,0,1,1).
  3. Tap × or press Esc: same exit, immediately.
  4. Tap Reload: toast exits; body.wiping — the lime panel translates from 100% to 0 over 520ms cubic-bezier(.2,0,0,1); at 300ms into that, the 56px “v2.4.1” label and “Kestrel · updated” line fade/rise in (200ms/300ms).
  5. At 1420ms after Reload: the header pill text becomes “v2.4.1” and turns lime; wiping is swapped for unwiping — the panel translates from 0 to −100% over 520ms cubic-bezier(.4,0,1,1), revealing the page.
  6. At 2000ms unwiping is removed; the panel is parked below the viewport again.
  7. Tap Simulate update (40px outline button, bottom-left, visible once the toast has gone): version pill resets to “v2.4.0”, the toast re-enters (rise from 24px, 420ms cubic-bezier(.16,1,.3,1); opacity 200ms) and a fresh 10s countdown starts.
  8. Task rows toggle their checkbox (lime fill, strikethrough, “done” label) on tap; the nav switches its current item. Neither affects the toast.

Structure

390 × 844
┌────────────────────────────────────┐
│ (54px clearance)                   │
│ Kestrel                  (v2.4.0)  │  h1 28px/800 · version pill
│ Tuesday · 5 open, 2 done           │
│ ┌────────────────────────────────┐ │
│ │ ☑ Ship release notes…    done  │ │  task rows 56px, 14px radius
│ │ ☑ Reply to Loam Studio…  done  │ │
│ │ ☐ Review the drawer scrim 10:30│ │
│ │ ☐ Book dentist…          today │ │
│ │ ...                            │ │
│ └────────────────────────────────┘ │
│ ╭────────────────────────────────╮ │
│ │ ▣ New version available        │ │  toast, bottom 112, 16px radius
│ │   v2.4.1 · fixes offline sync  │ │  [Reload] 36px · (×) ring 36px
│ ╰────────────────────────────────╯ │
│ ✓ Today  ▦ Upcoming  ≡ Lists  ⚙ Settings │  nav 64px
│ (34px clearance)                   │
└────────────────────────────────────┘
      wipe: full-screen lime panel, "v2.4.1" 56px centred
  • <header class="top"> — <h1> + .ver pill. <p class="sub"> line.
  • <ul class="tasks"> — <li><button role="checkbox" aria-checked> with .box (22px), label span, <small> time.
  • <button class="again"> — absolute, left:16px; bottom:112px.
  • <div class="toast" role="status" aria-live="polite"> — padding 12px 6px 12px 14px, 10px gaps; .ic tile 36px, .txt (<b> + <small>), <button class="act">Reload, <button class="dis"> containing an SVG ring (.tr track + .pr progress) and a 14px × glyph.
  • <div class="wipe" aria-hidden> — absolute inset:0, z-index 5, <b> version and <small> caption.
  • <nav class="nav" aria-label="Primary"> — four buttons.

Tokens

:root {
  /* charcoal neutrals */
  --bg: #191a1c;
  --panel: #212326;        /* task rows, nav */
  --panel-2: #2a2d31;      /* row hover */
  --line: #33373c;
  --ink: #f1f0ec;
  --ink-2: #a9aaa4;
  --ink-3: #6f716c;        /* idle nav, checkbox border, times */

  /* accent — lime */
  --lime: #c8f04c;         /* checkbox fill, current nav, toast tile, wipe panel */
  --lime-ink: #1b2400;

  /* toast is light on dark */
  --toast: #f1f0ec;
  --toast-ink: #191a1c;
  --toast-sub: #5f615c;
  --ring-track: rgba(25,26,28,.12);

  /* type */
  --font: "Bricolage Grotesque", system-ui, sans-serif;   /* opsz 12..96, wght 400/600/800 */
  --fs-h1: 28px; --fs-wipe: 56px; --fs-body: 15px; --fs-toast: 14px; --fs-btn: 13px; --fs-meta: 12px; --fs-nav: 11px;

  /* layout */
  --nav-h: 64px;
  --bottom-clear: 34px;
  --toast-bottom: 112px;   /* 34 + 64 + 14 */
  --r-toast: 16px;
  --r-btn: 10px;
  --r-row: 14px;
  --r-box: 7px;
  --ring: 36px; --ring-r: 16; --ring-c: 100.5;

  /* elevation */
  --shadow-toast: 0 12px 32px rgba(0,0,0,.45);

  /* motion */
  --t-micro: 160ms;
  --t-toast: 420ms;
  --t-exit: 240ms;
  --t-count: 10s;
  --t-wipe: 520ms;
  --t-hold: 900ms;
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-std: cubic-bezier(.2, .7, .2, 1);
  --ease-in: cubic-bezier(.4, 0, 1, 1);
  --ease-emph: cubic-bezier(.2, 0, 0, 1);
}

Typography

RoleFamilySizeWeightLine-heightTrackingCase
App titleBricolage Grotesque28px8001−0.02emsentence
Wipe versionBricolage Grotesque56px8001−0.04emas is
Toast titleBricolage Grotesque14px6001.20sentence, nowrap
Toast sublineBricolage Grotesque12px4001.40sentence
Reload buttonBricolage Grotesque13px60010sentence
Task labelBricolage Grotesque15px4001.40sentence
Task timeBricolage Grotesque12px60010lowercase
Version pillBricolage Grotesque12px60010as is
Nav labelBricolage Grotesque11px60010sentence

Motion

ElementTriggerPropertyFrom → ToDurationEasingDelay
.toastshowtranslateY, opacity24px, 0 → 0, 1420 / 200ms--ease-out / linear0
.toasthidetranslateY, opacity0, 1 → 24px, 0240ms--ease-in0
.ring .prshowstroke-dashoffset0 → 100.510slinear0, forwards
.wipereloadtranslateY100% → 0520ms--ease-emph0
.wipe b/smallreloadopacity, translateY0, 10px → 1, 0200 / 300ms--ease-out300ms
.wipeunwipetranslateY0 → −100%520ms--ease-in1420ms after reload (JS)
.verunwipecolor, border-colorink-3 → lime160mslinearat 1420ms
.boxtogglebackground, bordertransparent → lime160mslinear0
.act:activepressscale1 → .970—instant

Reduced motion: transitions and animations 1ms; the ring shows full (no countdown drawing) but the 10s auto-dismiss timer still runs; the wipe still cuts to lime and back on the same JS timings.

States

  • Toast shown: .toast.on — interactive, ring animating, auto timer armed.
  • Toast hidden: no .on — pointer-events:none, “Simulate update” reachable.
  • Wiping / unwiping: body.wiping then body.unwiping; the panel is aria-hidden and pointer-events:none throughout.
  • Version pill updated: .ver.new — lime text and border.
  • Task checked: aria-checked="true", box filled lime with dark check, label strikethrough in --ink-3, time reads “done”.
  • Nav current: aria-current="page", lime.
  • Row hover: --panel-2.
  • Focus-visible: 3px lime outline, 2px offset, on all buttons.

Accessibility

  • Toast is role="status" aria-live="polite" so “New version available v2.4.1 · fixes offline sync” is announced once; it does not steal focus.
  • Dismiss button label: “Dismiss, auto-closes in 10 seconds”. Esc also dismisses.
  • Reload is a plain <button>; in production it posts SKIP_WAITING to the waiting worker and calls location.reload() on controllerchange.
  • The wipe panel is aria-hidden decoration; the version pill text change is the accessible signal of the update.
  • Tasks are role="checkbox" buttons; the time cell text (“done”) updates with the state.
  • Contrast: --toast-sub on --toast 5.6:1; --ink-2 on --bg 8.3:1; --lime-ink on --lime 13.4:1; --ink-3 on --panel 4.5:1.
  • Hit targets: Reload 36px tall (give it a 44px ::after hit area on touch), dismiss 36px visual within a 44px row, tasks 56px, nav ≥ 88×52.

Responsive rules

  • 390 wide: as specified.
  • 360 wide: toast subline may truncate — set white-space:nowrap; overflow:hidden; text-overflow:ellipsis on .txt small.
  • ≥ 600 wide: toast caps at max-width:420px, anchored bottom-left of the content column at 24px insets; the wipe stays full-viewport.
  • Tablet with a left rail: --toast-bottom becomes 24px (no bottom nav to clear).

Acceptance checklist

  • Toast is #F1F0EC on the dark page, 16px radius, at bottom:112px with 16px side insets, casting 0 12px 32px rgba(0,0,0,.45).
  • Toast enters over 420ms cubic-bezier(.16,1,.3,1) from 24px below; exits over 240ms cubic-bezier(.4,0,1,1).
  • Countdown ring is a 36px SVG (r=16) whose stroke-dashoffset goes 0 → 100.5 over exactly 10s, and the toast auto-dismisses at 10s.
  • Re-showing the toast restarts the ring from full (forced reflow between class removal and re-add).
  • × and Esc dismiss immediately.
  • Reload sweeps a #C8F04C panel up over 520ms cubic-bezier(.2,0,0,1), shows “v2.4.1” at 56px/800, holds 900ms, then sweeps off the top over 520ms.
  • The header version pill reads “v2.4.0” before and “v2.4.1” in lime after the wipe.
  • “Simulate update” resets the pill to v2.4.0 and re-shows the toast with a new countdown.
  • Toast has role="status"; dismiss button label mentions the auto-close.
  • Task checkboxes toggle with lime fill and strikethrough; nav current item is lime.
  • Focus rings visible on every button.
  • Reduced motion keeps the 10s timer and the reload sequence but removes drawn motion.

Implementation notes

Countdown ring — one circle, dasharray equal to the circumference, offset animated over the dismiss duration:

.ring { position:absolute; inset:0; width:36px; height:36px; transform:rotate(-90deg); }
.ring circle { fill:none; stroke-width:2; }
.ring .pr { stroke:var(--toast-ink); stroke-dasharray:100.5; stroke-dashoffset:0; stroke-linecap:round; }
.toast.on .ring .pr { animation: count 10s linear forwards; }
@keyframes count { to { stroke-dashoffset:100.5 } }

Restart the ring on re-show by forcing a reflow between removing and re-adding the class:

function show() {
  clearTimeout(auto);
  toast.classList.remove('on'); void toast.offsetWidth; toast.classList.add('on');
  auto = setTimeout(hide, 10000);
}

Two-phase wipe with keyframes, not transitions, so the exit direction differs from the entry:

.wipe { position:absolute; inset:0; background:var(--lime); transform:translateY(100%); z-index:5; pointer-events:none; }
.wiping   .wipe { animation: wipe 520ms var(--ease-emph) forwards; }
.unwiping .wipe { transform:none; animation: unwipe 520ms var(--ease-in) forwards; }
@keyframes wipe   { to { transform:none } }
@keyframes unwipe { to { transform:translateY(-100%) } }

Common mistakes: placing the toast over the nav instead of above it; animating the ring with JS intervals (drift, and it fights reduced-motion); forgetting forwards so the ring snaps back to full at 10s just before the toast disappears.

Details

Palette
Type
Bricolage Grotesque
Motion
Rich motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

60fps

Show one small moment at a time, and let the tags do the sorting.

All sources