Warranty Coverage Panel
A verdict for this serial number today, with exclusions given equal weight to cover and an extension offered only while it can still be bought.
Where the money is and who is holding it - four stages, each naming the responsible party, including the bank stage every where-is-my-refund contact comes from.
components/refund-status-tracker.tsxnpx hoverlab add refund-status-tracker
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
Refund REF-20418
£928.00
Back to Visa ending 4417. Currently: Refund released.
With Northwind
Three lines from order ORD-88410, with photographs attached.
With Northwind · Rotterdam DC
Two of three lines arrived. The lamp is still with the carrier.
With Northwind
Sent to the card ending 4417. From this point the money is with your bank, not with us.
With Your bank
Most banks post within 3–5 working days. We cannot make this faster and neither can our payment provider.
Nothing has gone wrong if the last stage is still open on day three. Banks post refunds in batches, and a weekend does not count as a working day.
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
/**
* <RefundStatusTracker> — where the money is, and who is holding it.
*
* A refund is the only part of e-commerce where the customer is waiting and
* the company has already been paid, which is why "Refund processed" as a
* final state is so infuriating: processed by whom, and why is the money not
* there.
*
* THE BLOCK'S ONE IDEA: NAME THE PARTY THAT HAS IT NOW
*
* Four stages, and each one says who is responsible and how long they
* typically take. The last stage is the important one and it belongs to the
* *bank*, not the retailer — the merchant releases the money on day one and it
* lands on day four, and that gap is where every "where is my refund" contact
* comes from. Attributing it correctly is not passing the buck; it is the
* difference between "we are ignoring you" and "your bank posts on Tuesdays".
*
* WHY A VERTICAL RAIL AND NOT A HORIZONTAL STEPPER
*
* Each stage carries a date, an actor and a sentence of explanation. That does
* not fit under a horizontal node at any width, and the responsive fallback of
* a horizontal stepper is always a vertical one, so it may as well be built
* that way once. `order-tracking-timeline` in this catalog takes the same
* shape for the same reason.
*
* THE FAILED STATE IS DRAWN, NOT LEFT AS AN EXERCISE. A refund can bounce —
* a closed card is common on anything bought more than 90 days ago — and the
* recovery is a form, not a support email. Give any stage `state: 'failed'`
* and the recovery panel replaces the reassurance note at the foot.
*
* SERVER COMPONENT. Nothing here changes without a page load; a status tracker
* that hydrates to render the same markup is paying for JavaScript twice.
*
* ACCESSIBILITY: an ordered list with the current stage marked by
* `aria-current="step"`; each state carries a word, not just a colour or an
* icon; the summary at the top repeats the outcome so it is reachable without
* walking the list.
*/
import * as React from 'react'
import { Building2, Check, CircleAlert, Clock, Landmark, Store } from 'lucide-react'
export type StageState = 'done' | 'current' | 'waiting' | 'failed'
export interface RefundStage {
id: string
label: string
/** Who holds the refund at this stage. The point of the block. */
actor: string
actorKind: 'you' | 'retailer' | 'bank'
detail: string
when?: string
state: StageState
}
export interface RefundStatusTrackerProps {
reference?: string
amount?: string
method?: string
stages?: RefundStage[]
className?: string
}
const DEFAULT_STAGES: RefundStage[] = [
{
id: 'received',
label: 'Request received',
actor: 'Northwind',
actorKind: 'retailer',
detail: 'Three lines from order ORD-88410, with photographs attached.',
when: '3 September, 14:20',
state: 'done',
},
{
id: 'goods',
label: 'Returned goods checked in',
actor: 'Northwind · Rotterdam DC',
actorKind: 'retailer',
detail: 'Two of three lines arrived. The lamp is still with the carrier.',
when: '8 September, 09:05',
state: 'done',
},
{
id: 'released',
label: 'Refund released',
actor: 'Northwind',
actorKind: 'retailer',
detail:
'Sent to the card ending 4417. From this point the money is with your bank, not with us.',
when: '9 September, 11:40',
state: 'current',
},
{
id: 'posted',
label: 'Posted to your account',
actor: 'Your bank',
actorKind: 'bank',
detail:
'Most banks post within 3–5 working days. We cannot make this faster and neither can our payment provider.',
state: 'waiting',
},
]
const ACTOR_ICON: Record<RefundStage['actorKind'], typeof Store> = {
you: Building2,
retailer: Store,
bank: Landmark,
}
const STATE_DOT: Record<StageState, string> = {
done: 'bg-emerald-500 text-white',
current: 'bg-primary text-primary-foreground',
waiting: 'bg-muted text-muted-foreground',
failed: 'bg-destructive text-destructive-foreground',
}
const STATE_WORD: Record<StageState, string> = {
done: 'Done',
current: 'In progress',
waiting: 'Waiting',
failed: 'Failed',
}
export function RefundStatusTracker({
reference = 'REF-20418',
amount = '£928.00',
method = 'Visa ending 4417',
stages = DEFAULT_STAGES,
className = '',
}: RefundStatusTrackerProps) {
const failed = stages.find((stage) => stage.state === 'failed')
const current = stages.find((stage) => stage.state === 'current')
return (
<section className={`bg-background px-4 py-10 sm:px-6 ${className}`}>
<div className="mx-auto max-w-2xl">
<header className="rounded-2xl border border-border bg-card p-5">
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Refund {reference}
</p>
<p className="mt-1 text-2xl font-bold tracking-tight">{amount}</p>
<p className="mt-1 text-sm text-muted-foreground">
Back to {method}.{' '}
{failed
? 'The payment bounced — see below.'
: current
? `Currently: ${current.label}.`
: 'Complete.'}
</p>
</header>
<ol className="mt-6 space-y-1">
{stages.map((stage, index) => {
const ActorIcon = ACTOR_ICON[stage.actorKind]
const last = index === stages.length - 1
return (
<li
key={stage.id}
aria-current={stage.state === 'current' ? 'step' : undefined}
className="flex gap-3"
>
<div className="flex flex-col items-center">
<span
aria-hidden
className={`grid h-7 w-7 shrink-0 place-items-center rounded-full ${STATE_DOT[stage.state]}`}
>
{stage.state === 'done' ? (
<Check className="h-4 w-4" />
) : stage.state === 'failed' ? (
<CircleAlert className="h-4 w-4" />
) : (
<Clock className="h-3.5 w-3.5" />
)}
</span>
{/* border-s, not border-l: the rail follows the text. */}
{last ? null : (
<span
aria-hidden
className="my-1 w-px flex-1 border-s border-border"
/>
)}
</div>
<div className={`min-w-0 flex-1 ${last ? 'pb-0' : 'pb-6'}`}>
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
{stage.label ? (
<h3 data-stress-ignore className="text-sm font-semibold">{stage.label}</h3>
) : null}
<span className="text-xs text-muted-foreground">
{STATE_WORD[stage.state]}
</span>
{stage.when ? (
<span className="text-xs text-muted-foreground">· {stage.when}</span>
) : null}
</div>
{/* The point of the block. */}
<p className="mt-1 inline-flex items-center gap-1.5 rounded bg-muted px-1.5 py-0.5 text-xs font-medium">
<ActorIcon aria-hidden className="h-3 w-3" />
With {stage.actor}
</p>
<p className="mt-1.5 text-sm text-muted-foreground">{stage.detail}</p>
</div>
</li>
)
})}
</ol>
{failed ? (
<div
role="alert"
className="mt-6 rounded-xl border border-destructive/40 bg-destructive/5 p-4"
>
<h3 data-stress-ignore className="text-sm font-semibold text-destructive">
The card refused the refund
</h3>
<p className="mt-1 text-sm text-muted-foreground">
This usually means the card has been cancelled or replaced. Give us
bank details and the refund is re-sent within one working day — it
is a form, not an email to support.
</p>
<button
type="button"
className="mt-3 rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
Give alternative bank details
</button>
</div>
) : (
<p className="mt-6 rounded-xl border border-dashed border-border p-4 text-xs text-muted-foreground">
Nothing has gone wrong if the last stage is still open on day three.
Banks post refunds in batches, and a weekend does not count as a
working day.
</p>
)}
</div>
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/refund-status-tracker.tsx and import it where you need the section:
import { RefundStatusTracker } from '@/components/refund-status-tracker'3 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
reference | string | 'REF-20418' |
amount | string | '£928.00' |
method | string | 'Visa ending 4417' |
stages | RefundStage[] | DEFAULT_STAGES |
className | string | '' |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
<!--
Refund Status Tracker — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block has no interactive behaviour, so nothing is missing.
-->
<section class="bg-background px-4 py-10 sm:px-6 ">
<div class="mx-auto max-w-2xl">
<header class="rounded-2xl border border-border bg-card p-5">
<p class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Refund REF-20418</p>
<p class="mt-1 text-2xl font-bold tracking-tight">£928.00</p>
<p class="mt-1 text-sm text-muted-foreground">Back to Visa ending 4417. Currently: Refund released.</p>
</header>
<ol class="mt-6 space-y-1">
<li class="flex gap-3">
<div class="flex flex-col items-center">
<span aria-hidden="true" class="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-emerald-500 text-white">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check h-4 w-4" aria-hidden="true">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
<span aria-hidden="true" class="my-1 w-px flex-1 border-s border-border"></span>
</div>
<div class="min-w-0 flex-1 pb-6">
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<h3 data-stress-ignore="true" class="text-sm font-semibold">Request received</h3>
<span class="text-xs text-muted-foreground">Done</span>
<span class="text-xs text-muted-foreground">· 3 September, 14:20</span>
</div>
<p class="mt-1 inline-flex items-center gap-1.5 rounded bg-muted px-1.5 py-0.5 text-xs font-medium"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-store h-3 w-3" aria-hidden="true"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"></path><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"></path><path d="M2 7h20"></path><path d="M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"></path></svg>With Northwind</p>
<p class="mt-1.5 text-sm text-muted-foreground">Three lines from order ORD-88410, with photographs attached.</p>
</div>
</li>
<li class="flex gap-3">
<div class="flex flex-col items-center">
<span aria-hidden="true" class="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-emerald-500 text-white">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check h-4 w-4" aria-hidden="true">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</span>
<span aria-hidden="true" class="my-1 w-px flex-1 border-s border-border"></span>
</div>
<div class="min-w-0 flex-1 pb-6">
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<h3 data-stress-ignore="true" class="text-sm font-semibold">Returned goods checked in</h3>
<span class="text-xs text-muted-foreground">Done</span>
<span class="text-xs text-muted-foreground">· 8 September, 09:05</span>
</div>
<p class="mt-1 inline-flex items-center gap-1.5 rounded bg-muted px-1.5 py-0.5 text-xs font-medium"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-store h-3 w-3" aria-hidden="true"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"></path><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"></path><path d="M2 7h20"></path><path d="M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"></path></svg>With Northwind · Rotterdam DC</p>
<p class="mt-1.5 text-sm text-muted-foreground">Two of three lines arrived. The lamp is still with the carrier.</p>
</div>
</li>
<li aria-current="step" class="flex gap-3">
<div class="flex flex-col items-center">
<span aria-hidden="true" class="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-primary text-primary-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3.5 w-3.5" aria-hidden="true">
<path d="M12 6v6l4 2"></path>
<circle cx="12" cy="12" r="10"></circle>
</svg>
</span>
<span aria-hidden="true" class="my-1 w-px flex-1 border-s border-border"></span>
</div>
<div class="min-w-0 flex-1 pb-6">
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<h3 data-stress-ignore="true" class="text-sm font-semibold">Refund released</h3>
<span class="text-xs text-muted-foreground">In progress</span>
<span class="text-xs text-muted-foreground">· 9 September, 11:40</span>
</div>
<p class="mt-1 inline-flex items-center gap-1.5 rounded bg-muted px-1.5 py-0.5 text-xs font-medium"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-store h-3 w-3" aria-hidden="true"><path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"></path><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"></path><path d="M2 7h20"></path><path d="M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"></path></svg>With Northwind</p>
<p class="mt-1.5 text-sm text-muted-foreground">Sent to the card ending 4417. From this point the money is with your bank, not with us.</p>
</div>
</li>
<li class="flex gap-3">
<div class="flex flex-col items-center">
<span aria-hidden="true" class="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-muted text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3.5 w-3.5" aria-hidden="true">
<path d="M12 6v6l4 2"></path>
<circle cx="12" cy="12" r="10"></circle>
</svg>
</span>
</div>
<div class="min-w-0 flex-1 pb-0">
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
<h3 data-stress-ignore="true" class="text-sm font-semibold">Posted to your account</h3>
<span class="text-xs text-muted-foreground">Waiting</span>
</div>
<p class="mt-1 inline-flex items-center gap-1.5 rounded bg-muted px-1.5 py-0.5 text-xs font-medium"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-landmark h-3 w-3" aria-hidden="true"><path d="M10 18v-7"></path><path d="M11.12 2.198a2 2 0 0 1 1.76.006l7.866 3.847c.476.233.31.949-.22.949H3.474c-.53 0-.695-.716-.22-.949z"></path><path d="M14 18v-7"></path><path d="M18 18v-7"></path><path d="M3 22h18"></path><path d="M6 18v-7"></path></svg>With Your bank</p>
<p class="mt-1.5 text-sm text-muted-foreground">Most banks post within 3–5 working days. We cannot make this faster and neither can our payment provider.</p>
</div>
</li>
</ol>
<p class="mt-6 rounded-xl border border-dashed border-border p-4 text-xs text-muted-foreground">Nothing has gone wrong if the last stage is still open on day three. Banks post refunds in batches, and a weekend does not count as a working day.</p>
</div>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse After-Sale ServiceCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
A verdict for this serial number today, with exclusions given equal weight to cover and an extension offered only while it can still be bought.
Refunds are per line, not per order: a reason on each line that drives what is asked for next, and a running total itemising the restocking fee and the delivery rule.
The operator queue, sorted oldest-open-first because recency is the wrong default, with blocked as a first-class status that names its blocker.
Refund, exchange or credit decided per line, with live stock on the variants and the price difference shown before the goods are posted rather than after.
The screen between approved and posted: three drop-off methods with the thing that actually blocks each one, the packing rules that get returns refused, and a date rather than a duration.
A repair is not a return - a symptom picker that filters the service routes, warranty resolved inside the form, and an estimate with its uncertainty stated.
The reviews you wrote and the ones you owe, with editing that is honest about being marked as edited and moderation that names the rule it applied.
A balance checker built for the two ways it goes wrong - an empty card and an expired one - answering with a transaction history rather than a bare zero.