Nº 227 of 520
Download button with progress and retry
A download button that becomes a live progress bar with size, speed and time left, pause and resume, a drawn check on done, and resumable retry. Asset rows use rings.
Preview
Use it
Free to use · credit appreciated1 Once, in your project
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Download button with progress and retry" (piece button-download-progress) 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.
<!-- Design Lounge Nº 227 · "Download button with progress and retry" · designlounge.vercel.app -->
# Download button with progress and retry
> **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 release download card for "pocketknife", a fictional command-line task runner, on a near-black grid page. The hero is a 60px acid-lime button, "Download for macOS · arm64 · 18.4 MB". Clicking it turns the same 60px row into a progress bar with a moving stripe. The bar reads "Downloading 41%" on the left and "7.5 / 18.4 MB · 3.3 MB/s · 4s left" on the right. Pause and cancel squares grow out of the right edge. Done fills the bar solid, draws a check and names the file. A dropped connection turns the bar coral and keeps the bytes, and the pause square becomes Retry, which resumes from where it stopped. Below, three asset rows use a 40px progress ring as their button: download, pause, resume, retry, done.
The detail worth copying is the label that **inverts where the fill passes**. The text is drawn twice, light on the track and dark inside the fill, and the fill's `clip-path` reveals the dark copy. Every character stays readable at every percent.
## Reference behaviour
1. First frame: a 680px panel. Header: 36px lime mark, "pocketknife" (24px/700), "v3.2.0" in mono, and a "latest" outline tag. Lede: "A pocket-sized task runner for monorepos. Released 2 Oct 2026."
2. The hero button is idle: lime fill, a download glyph, "Download for macOS", and "arm64 · 18.4 MB" right-aligned in mono at 75 % opacity. Below it, on the left, "sha256 7c1e4f…09a3b2" in mono. On the right, a switch "Simulate a dropped connection" (off).
3. "Other assets": linux-x64.tar.gz (17.9 MB) is **already downloading** from 38 % at about 2.2 MB/s, so the page is alive on load. windows-x64.zip (19.2 MB) is idle. checksums.txt (1.2 KB) is done, with a lime disc and a check.
4. Click the hero: the button fades (160ms) and scales X to .97. The bar fades in under it. The pause and cancel squares grow from width 0 to 60px with an 8px left margin (280ms). Focus moves to Pause.
5. While running, the fill width is the true fraction. The stripes (−45°, 8px dark at 9 % / 8px clear) slide continuously, 700ms per tile. The left label is "Downloading N%". The right label is "got / total MB · speed MB/s · Ns left". Speed wanders between 70 % and 130 % of 3.4 MB/s and is smoothed, so the number breathes rather than flickers.
6. **Pause:** the stripes stop, the fill dims to `#a9c447`, the label reads "Paused at N%" and "got / 18.4 MB", and the square shows a play glyph labelled "Resume download". Resume continues from the same byte.
7. **Cancel:** returns to the idle button at 0 and focuses it. The live region says "Download cancelled."
8. **Done** (18.4 of 18.4): the fill becomes solid lime with no stripes. A check draws (360ms after 120ms) before "Downloaded", with the file name "pocketknife-3.2.0-macos-arm64.pkg" on the right. The squares collapse to 0. Under the bar, the checksum is replaced by a lime "Download again" text button, which takes focus.
9. **Error:** with the switch on, the next download stops at exactly 47 %. The track turns `rgba(255,106,77,.14)` with a coral ring, and the fill turns coral with no stripes. The label reads "(!) Connection dropped" and "8.6 / 18.4 MB kept". The pause square becomes a coral Retry glyph and takes focus. Retry resumes from 8.6 MB. Each run fails at most once.
10. **Ring rows:** click idle to start (the ring arc draws, the meta shows "got / total MB · speed MB/s", the centre shows pause). Click to pause (the arc dims, "Paused · got / total MB", play glyph). Click to resume. windows-x64.zip fails once at 62 % (the arc goes coral, the meta reads "Mirror reset at 62% · tap to retry", retry glyph). Done: a lime disc scales in from 0 behind a drawn dark check, the meta reads "17.9 MB · verified", and the button becomes `aria-disabled`.
11. One `requestAnimationFrame` loop drives every running job and stops itself when nothing is running.
## Structure
```
1280 × 800, panel 680px centred, 48px grid page with radial vignette
┌ panel ─────────────────────────────────────────────────────────┐ padding 28
│ [■] pocketknife v3.2.0 (latest) │
│ A pocket-sized task runner for monorepos. Released 2 Oct 2026. │
│ ┌ main (flex 1) ─────────────────────────────┐ ┌60┐ ┌60┐ │ 60px
│ │▓▓▓▓▓▓▓ Downloading 41% ░ 7.5 / 18.4 MB · …│ │ ‖│ │ ×│ │
│ └────────────────────────────────────────────┘ └──┘ └──┘ │
│ sha256 7c1e4f…09a3b2 (o) Simulate a dropped conn. │
│ OTHER ASSETS │
│ [▤] pocketknife-3.2.0-linux-x64.tar.gz ( ring 40 ) │ 12px rows
│ 15.0 MB / 17.9 MB · 2.2 MB/s │
… 186 more lines. Copy or download for the full brief. Loading the source…Brief
226 linesBuild 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 release download card for “pocketknife”, a fictional command-line task runner, on a near-black grid page. The hero is a 60px acid-lime button, “Download for macOS · arm64 · 18.4 MB”. Clicking it turns the same 60px row into a progress bar with a moving stripe. The bar reads “Downloading 41%” on the left and “7.5 / 18.4 MB · 3.3 MB/s · 4s left” on the right. Pause and cancel squares grow out of the right edge. Done fills the bar solid, draws a check and names the file. A dropped connection turns the bar coral and keeps the bytes, and the pause square becomes Retry, which resumes from where it stopped. Below, three asset rows use a 40px progress ring as their button: download, pause, resume, retry, done.
The detail worth copying is the label that inverts where the fill passes. The text is drawn twice, light on the track and dark inside the fill, and the fill’s clip-path reveals the dark copy. Every character stays readable at every percent.
Reference behaviour
- First frame: a 680px panel. Header: 36px lime mark, “pocketknife” (24px/700), “v3.2.0” in mono, and a “latest” outline tag. Lede: “A pocket-sized task runner for monorepos. Released 2 Oct 2026.”
- The hero button is idle: lime fill, a download glyph, “Download for macOS”, and “arm64 · 18.4 MB” right-aligned in mono at 75 % opacity. Below it, on the left, “sha256 7c1e4f…09a3b2” in mono. On the right, a switch “Simulate a dropped connection” (off).
- “Other assets”: linux-x64.tar.gz (17.9 MB) is already downloading from 38 % at about 2.2 MB/s, so the page is alive on load. windows-x64.zip (19.2 MB) is idle. checksums.txt (1.2 KB) is done, with a lime disc and a check.
- Click the hero: the button fades (160ms) and scales X to .97. The bar fades in under it. The pause and cancel squares grow from width 0 to 60px with an 8px left margin (280ms). Focus moves to Pause.
- While running, the fill width is the true fraction. The stripes (−45°, 8px dark at 9 % / 8px clear) slide continuously, 700ms per tile. The left label is “Downloading N%”. The right label is “got / total MB · speed MB/s · Ns left”. Speed wanders between 70 % and 130 % of 3.4 MB/s and is smoothed, so the number breathes rather than flickers.
- Pause: the stripes stop, the fill dims to
#a9c447, the label reads “Paused at N%” and “got / 18.4 MB”, and the square shows a play glyph labelled “Resume download”. Resume continues from the same byte. - Cancel: returns to the idle button at 0 and focuses it. The live region says “Download cancelled.”
- Done (18.4 of 18.4): the fill becomes solid lime with no stripes. A check draws (360ms after 120ms) before “Downloaded”, with the file name “pocketknife-3.2.0-macos-arm64.pkg” on the right. The squares collapse to 0. Under the bar, the checksum is replaced by a lime “Download again” text button, which takes focus.
- Error: with the switch on, the next download stops at exactly 47 %. The track turns
rgba(255,106,77,.14)with a coral ring, and the fill turns coral with no stripes. The label reads ”(!) Connection dropped” and “8.6 / 18.4 MB kept”. The pause square becomes a coral Retry glyph and takes focus. Retry resumes from 8.6 MB. Each run fails at most once. - Ring rows: click idle to start (the ring arc draws, the meta shows “got / total MB · speed MB/s”, the centre shows pause). Click to pause (the arc dims, “Paused · got / total MB”, play glyph). Click to resume. windows-x64.zip fails once at 62 % (the arc goes coral, the meta reads “Mirror reset at 62% · tap to retry”, retry glyph). Done: a lime disc scales in from 0 behind a drawn dark check, the meta reads “17.9 MB · verified”, and the button becomes
aria-disabled. - One
requestAnimationFrameloop drives every running job and stops itself when nothing is running.
Structure
1280 × 800, panel 680px centred, 48px grid page with radial vignette
┌ panel ─────────────────────────────────────────────────────────┐ padding 28
│ [■] pocketknife v3.2.0 (latest) │
│ A pocket-sized task runner for monorepos. Released 2 Oct 2026. │
│ ┌ main (flex 1) ─────────────────────────────┐ ┌60┐ ┌60┐ │ 60px
│ │▓▓▓▓▓▓▓ Downloading 41% ░ 7.5 / 18.4 MB · …│ │ ‖│ │ ×│ │
│ └────────────────────────────────────────────┘ └──┘ └──┘ │
│ sha256 7c1e4f…09a3b2 (o) Simulate a dropped conn. │
│ OTHER ASSETS │
│ [▤] pocketknife-3.2.0-linux-x64.tar.gz ( ring 40 ) │ 12px rows
│ 15.0 MB / 17.9 MB · 2.2 MB/s │
│ [▤] pocketknife-3.2.0-windows-x64.zip ( ring ) │
│ [▤] checksums.txt 1.2 KB · verified ( disc + check ) │
└────────────────────────────────────────────────────────────────┘
.dlis a 60px flex row:.main(flex 1,position: relative) holdsbutton.goanddiv.bar[role=progressbar], bothinset: 0. Thenbutton#pause.ctlandbutton#cancel.ctl..barcontains.txt(light label) and.fill(lime, clipped)..fillcontains a second.txtwith the same spans in dark ink. JS writes both copies..underis the checksum or “Download again”, plus the switch (<label>wrappingbutton[role=switch]).<h2>thenul.rows > li.row: file icon (32px tile),.fname(mono name + mono meta), andbutton.ring. Inside the ring:.disc, an SVG with a track and arc circles (r=17, rotated −90°), and an icon SVG.- One visually hidden polite live region.
Tokens
:root {
--bg: #0b0d0a; /* page, 48px grid lines in --line, radial vignette to --bg at 70% */
--surface: #121510; /* panel */
--surface-2: #1a1e17; /* bar track, control squares, file tiles */
--line: #262c22;
--ink: #e4e9d8;
--ink-2: #a3ac95;
--ink-3: #7f8872;
--acid: #c8f04a; /* the one accent: button, fill, ring arc, done disc, focus */
--acid-dim: #a9c447; /* paused fill and arc */
--on-acid: #10140a;
--err: #ff6a4d;
--err-soft: rgba(255, 106, 77, .14);
--sans: "Epilogue", system-ui, sans-serif;
--mono: "Fragment Mono", ui-monospace, monospace;
--r: 10px; /* button, bar, squares */
--r-panel: 16px;
--h: 60px; /* hero row */
--ring: 40px; /* ring svg; button is 44px */
--ring-c: 106.8; /* 2π·17 */
--t-micro: 160ms;
--t-morph: 280ms;
--t-check: 360ms;
--t-stripe: 700ms;
--ease: cubic-bezier(.2, .7, .2, 1);
--ease-out: cubic-bezier(.16, 1, .3, 1);
}
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Notes |
|---|---|---|---|---|---|---|
| Product name | Epilogue | 24px | 700 | 1 | −0.02em | |
| Lede | Epilogue | 15px | 500 | 1.5 | 0 | --ink-2 |
| Hero label | Epilogue | 16px | 700 | 1 | 0 | --on-acid |
| Bar status | Epilogue | 15px | 600 | 1 | 0 | “Downloading 41%” |
| Bar meta | Fragment Mono | 13px | 400 | 1 | 0 | bytes · speed · eta, ellipsis |
| Version, tag, sha | Fragment Mono | 12–13px | 400 | 1.5 | 0 | |
| Section label | Epilogue | 12px | 600 | 1 | 0.1em | UPPERCASE --ink-3 |
| File name | Fragment Mono | 13.5px | 400 | 1.4 | 0 | ellipsis |
| File meta | Fragment Mono | 12.5px | 400 | 1.4 | 0 | --ink-3, error --err |
| Switch label | Epilogue | 13px | 500 | 1 | 0 | --ink-2 |
Numbers are mono so the bytes and speed don’t jitter sideways while ticking.
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Reduced motion |
|---|---|---|---|---|---|---|
.go | start | opacity, scaleX | 1, 1 → 0, .97 | 160 / 280ms | --ease / --ease-out | 1ms |
.bar | start | opacity | 0 → 1 | 160ms | --ease | 1ms |
.ctl ×2 | start / end | width, margin-left, opacity | 0, 0, 0 ↔ 60px, 8px, 1 | 280ms | --ease-out | 1ms |
.fill | progress | clip-path right inset | 100% → 0% | per frame | n/a (data) | kept, it is information |
.fill stripes | running | background-position | 0 → 22.63px | 700ms loop | linear | static |
| hero check | done | stroke-dashoffset | 22 → 0 | 360ms, delay 120ms | --ease | drawn |
| ring arc | progress | stroke-dashoffset | 106.8 → 0 | per frame | n/a | kept |
ring .disc | done | scale | 0 → 1 | 280ms | --ease-out | 1ms |
| ring check | done | stroke-dashoffset | 22 → 0 | 360ms, delay 120ms | --ease | drawn |
| switch knob | toggle | translateX | 0 → 16px | 160ms | --ease | 1ms |
| track/fill colour | pause, error | background-color | lime → dim / coral | 280ms | --ease | 1ms |
The stripe is the only loop, and it runs only while bytes are moving. It stops on pause, error, done and reduced motion.
States
- Idle: lime button. Hover
brightness(1.06). Active scale .985. - Running: striped lime fill over the
--surface-2track with a 1px--linering. Pause and cancel squares are visible. - Paused: fill
#a9c447, stripes still. Play glyph, label “Resume download”. - Error: coral-tinted track and ring, coral fill without stripes, coral text on the track side and
#1a0c08inside the fill. Retry glyph in a coral-ringed square. - Done: solid lime, drawn check, file name. The squares collapse, and “Download again” replaces the checksum.
- Ring idle / running / paused / error / done: download glyph / pause + lime arc / play + dim arc / retry + coral arc and coral meta / lime disc + dark check +
aria-disabled. - Ring hover: the track goes
#3a4234(not when done). - Switch on: coral track, dark knob at 16px.
- Focus-visible: 2px
--acidoutline, 3px offset, on every control including the squares and rings.
Accessibility
- The hero idle button has a full label: “Download pocketknife for macOS, arm64, 18.4 megabytes”.
- The bar is
role="progressbar"witharia-valuemin=0,aria-valuemax=100, andaria-valuenowupdated only when the integer percent changes.aria-valuetextis “41%, 7.5 MB of 18.4 MB”. It isaria-hiddenwhile idle. - The squares are real buttons with labels that name the action: “Pause download”, “Resume download”, “Retry download”, “Cancel download”. They are
inertwhen collapsed. - Focus: start → Pause. Error → Retry. Done → “Download again”. Cancel or again → the hero button. Focus never falls to
<body>when a control collapses. - The live region announces: paused at N%, resumed, connection dropped at N% (retry resumes from X MB), download complete with file name and size, and each row’s downloaded or failed message. Progress ticks are not announced.
- Ring labels: “Download linux-x64.tar.gz, 17.9 MB”, “Pause linux-x64.tar.gz”, “Resume …”, “Retry …”, and ”… downloaded” (
aria-disabled="true", so it stays focusable and explains itself). - The switch is
role="switch"witharia-checkedand the visible label text. - Contrast:
--ink-3on--surfaceis 4.97:1.--on-acidon--acidis 14.2:1.--erron--surfaceis 6.5:1. Dark text on the coral fill is 6.75:1. - Hit targets: hero row 60px, squares 60 × 60, rings 44 × 44.
Responsive rules
- ≥ 1280: 680px panel, as specified.
- 1024–1279 and 768–1023: unchanged. The panel is
min(680px, 100%). - ≤ 520: panel padding 20px. The hero’s “arm64 · 18.4 MB” is hidden. The bar hides its right-hand meta and keeps “Downloading N%”, because the squares need 136px. The checksum and switch stack. File names truncate with an ellipsis, so the ring never wraps.
- The page grid must use
grid-template-columns: minmax(0, 1fr). Otherwise long mono file names set the min-content width and push the page past 375px.
Acceptance checklist
Always
- The idle button and the progress bar occupy the same row and height. The bar does not appear somewhere else.
- The label stays legible across the fill edge (two layers, clipped).
- Bytes, total, speed and time left are shown while running, in a monospace face.
- Pause keeps the bytes. Resume continues from them.
- Error keeps the bytes, explains what happened, and offers Retry in the same square as Pause.
- Done names the file and offers a way to download again.
- The stripe loop stops whenever bytes are not moving.
- One rAF loop for all jobs, stopped when idle. No
setIntervalticking. - Focus moves to the control that replaces the one that disappeared.
This demo
- Hero file: macOS arm64, 18.4 MB, about 3.4 MB/s (finishes in about 5–6s).
- Flaky switch: the next run fails at 47 % (“8.6 / 18.4 MB kept”).
- linux-x64.tar.gz starts at 38 % on load. windows-x64.zip fails once at 62 %. checksums.txt starts done.
- The accent is
#c8f04a, paused#a9c447, error#ff6a4d. - Squares are 60px. Ring radius 17, stroke 2.5, circumference 106.8.
Implementation notes
Inverting label with one clip. Render the label twice. The dark copy lives inside the fill, and the fill is clipped from the right by the remaining percent. Set --p on the bar each frame.
.bar { position: relative; overflow: hidden; background: var(--surface-2); border-radius: 10px; }
.txt { position: absolute; inset: 0; display: flex; align-items: center; gap: 16px; padding: 0 20px; white-space: nowrap; }
.fill { position: absolute; inset: 0; background-color: var(--acid); color: var(--on-acid);
clip-path: inset(0 calc(100% - var(--p, 0%)) 0 0);
background-image: repeating-linear-gradient(-45deg, rgba(16,20,10,.09) 0 8px, transparent 8px 16px);
background-size: 22.63px 22.63px; }
[data-state=running] .fill { animation: stripes 700ms linear infinite; }
@keyframes stripes { to { background-position: 22.63px 0; } }
22.63px is 16 × √2, which makes the −45° stripe tile with no visible join.
One loop, smoothed speed, single failure.
function tick(t) {
const dt = last ? Math.min(.1, (t - last) / 1000) : 0; last = t;
let any = false;
for (const j of jobs) {
if (j.state !== 'running') continue; any = true;
j.v += (j.base * (.7 + Math.random() * .6) - j.v) * Math.min(1, dt * 3); // ease toward a jittered target
j.got = Math.min(j.total, j.got + j.v * dt);
if (j.failAt && j.got >= j.failAt * j.total) { j.got = j.failAt * j.total; j.failAt = 0; j.set('error'); continue; }
if (j.got >= j.total) { j.set('done'); continue; }
j.paint();
}
raf = any ? requestAnimationFrame(tick) : (last = 0, 0);
}
Clamp dt to 100ms so a backgrounded tab doesn’t jump to done on return. In a real app, replace the simulator with fetch plus a ReadableStream reader for bytes, and with Range: bytes=N- for resume.
Ring. stroke-dasharray: 106.8; stroke-dashoffset: calc(106.8 * (1 - var(--f))), rotated −90° so it starts at 12 o’clock. Only rotate the ring SVG, not the centre icon. A broad .ring > svg selector turns the pause bars sideways.
Common mistakes:
- Putting the overlay bar on top of the idle button without
pointer-events: none. It silently eats the first click. - Animating
widthon the fill. Useclip-pathorscaleX, and keep the label layers aligned. - Proportional digits for bytes and speed. The line shimmies every frame.
- Restarting from zero on retry. Keep the bytes and say so (“8.6 / 18.4 MB kept”).
- Announcing every percent to screen readers.
- Leaving the stripe animating on a paused or failed bar. It tells the user something is moving when nothing is.
Details
- Palette
- Type
- Epilogue · Fragment Mono
- Motion
- Subtle motion
- Build
- Multi-step
- Tags
Studied
UseArticleLet the visitor press the controls and watch the card change. Thick black edges and hard shadows keep it friendly.
All sources