Skip to content
Commerce block

Refunds Overview Table

The operator queue, sorted oldest-open-first because recency is the wrong default, with blocked as a first-class status that names its blocker.

277 lineslucide-reactAdded 13 Sept 2026
  • refunds
  • table
  • queue
  • operations
  • after-sale

What's included

  • components/refunds-overview-table.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add refunds-overview-table

Or over MCP, from your editor's agent — no account needed.

License

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.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Refunds

Oldest open first. A refund gets slower the longer nobody looks at it, so recency is the wrong default here.

Released this period
£412.40
Past 5 days
3
Blocked on us
3
Refunds, by age
RefundCustomerStatus
REF-20390ORD-88104A. Ferreira£214.0013 days — past the promiseraised 31 AugBlockedBank details refused — customer contacted twice
REF-20402ORD-88221Northwind Trading£1,840.5011 days — past the promiseraised 2 SepBlockedOver the £1,000 approval limit — needs Rhea Patel
REF-20411ORD-88377K. Ojo£89.998 days — past the promiseraised 5 SepBlockedAwaiting returned goods — carrier scan 4 days ago
REF-20418ORD-88410M. Haugen£928.004 daysraised 9 SepApproved
REF-20425ORD-88502Contoso Logistics£412.402 daysraised 11 SepReleased
REF-20431ORD-88566S. Baptiste£64.001 dayraised 12 SepRejected

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/refunds-overview-table.tsx
'use client'

/**
 * <RefundsOverviewTable> — the operator's side of the refund queue.
 *
 * `refund-status-tracker` is what one customer sees about one refund. This is
 * what the finance or support team sees about all of them, and it is a
 * different design problem: the interesting rows are not the recent ones, they
 * are the *stuck* ones.
 *
 * SORTED BY AGE, NOT BY DATE
 *
 * Newest-first is the default of every table and the wrong default here. A
 * refund raised this morning needs nothing; one raised eleven days ago and
 * still unpaid is a complaint that has not been made yet. So the default order
 * is oldest-open-first, and the age column is styled by how far past the
 * promise it is rather than by absolute value.
 *
 * THE SUMMARY BAND IS THREE NUMBERS AND THEY ARE THE THREE THAT MATTER
 *
 * Money out this period, the count breaching the service promise, and the
 * count blocked on something the team can fix. Not a chart. A refunds queue is
 * worked from a list; the band exists to say whether the list needs working
 * today.
 *
 * BLOCKED IS A FIRST-CLASS STATUS AND IT NAMES THE BLOCKER
 *
 * "Awaiting goods", "bank details refused", "over approval limit" — three
 * different people fix those three things. A single "Pending" bucket makes the
 * queue unworkable and is the most common failure of this screen.
 *
 * ACCESSIBILITY: a real `<table>` with scoped headers and a caption; sorting
 * is a header button carrying `aria-sort`, which is the only thing that makes
 * the current order perceivable without sight; the scroll container is
 * `relative`, because `sr-only` is `position: absolute` and a static
 * `overflow-x-auto` does not clip it — the label escapes and scrolls the page
 * sideways on a phone.
 */

import * as React from 'react'
import { AlertTriangle, ArrowUpDown, Banknote, Clock } from 'lucide-react'

export type RefundStatus = 'released' | 'approved' | 'blocked' | 'rejected'

export interface RefundRow {
  id: string
  order: string
  customer: string
  amount: number
  raisedOn: string
  /** Days since it was raised. Drives the default sort and the urgency style. */
  ageDays: number
  status: RefundStatus
  /** Required when blocked: who or what unblocks it. */
  blocker?: string
}

export interface RefundsOverviewTableProps {
  rows?: RefundRow[]
  /** Days after which an open refund is breaching the promise. */
  promiseDays?: number
  currency?: string
  className?: string
}

const DEFAULT_ROWS: RefundRow[] = [
  { id: 'REF-20390', order: 'ORD-88104', customer: 'A. Ferreira', amount: 214.0, raisedOn: '31 Aug', ageDays: 13, status: 'blocked', blocker: 'Bank details refused — customer contacted twice' },
  { id: 'REF-20402', order: 'ORD-88221', customer: 'Northwind Trading', amount: 1840.5, raisedOn: '2 Sep', ageDays: 11, status: 'blocked', blocker: 'Over the £1,000 approval limit — needs Rhea Patel' },
  { id: 'REF-20411', order: 'ORD-88377', customer: 'K. Ojo', amount: 89.99, raisedOn: '5 Sep', ageDays: 8, status: 'blocked', blocker: 'Awaiting returned goods — carrier scan 4 days ago' },
  { id: 'REF-20418', order: 'ORD-88410', customer: 'M. Haugen', amount: 928.0, raisedOn: '9 Sep', ageDays: 4, status: 'approved' },
  { id: 'REF-20425', order: 'ORD-88502', customer: 'Contoso Logistics', amount: 412.4, raisedOn: '11 Sep', ageDays: 2, status: 'released' },
  { id: 'REF-20431', order: 'ORD-88566', customer: 'S. Baptiste', amount: 64.0, raisedOn: '12 Sep', ageDays: 1, status: 'rejected' },
]

const STATUS_STYLE: Record<RefundStatus, string> = {
  released: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400',
  approved: 'bg-primary/10 text-primary',
  blocked: 'bg-amber-500/10 text-amber-700 dark:text-amber-400',
  rejected: 'bg-muted text-muted-foreground',
}

const STATUS_LABEL: Record<RefundStatus, string> = {
  released: 'Released',
  approved: 'Approved',
  blocked: 'Blocked',
  rejected: 'Rejected',
}

type SortKey = 'ageDays' | 'amount'

export function RefundsOverviewTable({
  rows = DEFAULT_ROWS,
  promiseDays = 5,
  currency = '£',
  className = '',
}: RefundsOverviewTableProps) {
  // Oldest open first — see the docblock. Not newest-first.
  const [sort, setSort] = React.useState<{ key: SortKey; dir: 'asc' | 'desc' }>({
    key: 'ageDays',
    dir: 'desc',
  })
  const [only, setOnly] = React.useState<'all' | 'open'>('all')

  const open = (row: RefundRow) => row.status === 'blocked' || row.status === 'approved'

  const visible = React.useMemo(() => {
    const filtered = only === 'open' ? rows.filter(open) : rows
    return [...filtered].sort((a, b) => {
      const delta = a[sort.key] - b[sort.key]
      return sort.dir === 'asc' ? delta : -delta
    })
  }, [rows, only, sort])

  const outstanding = rows.filter(open)
  const breaching = outstanding.filter((row) => row.ageDays > promiseDays)
  const blocked = rows.filter((row) => row.status === 'blocked')
  const released = rows
    .filter((row) => row.status === 'released')
    .reduce((sum, row) => sum + row.amount, 0)

  function money(value: number) {
    return `${currency}${value.toLocaleString('en-GB', { minimumFractionDigits: 2 })}`
  }

  function toggleSort(key: SortKey) {
    setSort((s) => (s.key === key ? { key, dir: s.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'desc' }))
  }

  function ariaSort(key: SortKey) {
    if (sort.key !== key) return 'none' as const
    return sort.dir === 'asc' ? ('ascending' as const) : ('descending' as const)
  }

  return (
    <section className={`bg-background px-4 py-10 sm:px-6 ${className}`}>
      <div className="mx-auto max-w-5xl">
        <header className="flex flex-wrap items-end justify-between gap-4">
          <div>
            <h2 className="text-xl font-bold tracking-tight">Refunds</h2>
            <p className="mt-1 text-sm text-muted-foreground">
              Oldest open first. A refund gets slower the longer nobody looks at
              it, so recency is the wrong default here.
            </p>
          </div>
          <div className="flex rounded-lg border border-border p-0.5">
            {(['all', 'open'] as const).map((value) => (
              <button
                key={value}
                type="button"
                onClick={() => setOnly(value)}
                aria-pressed={only === value}
                className={`rounded-md px-2.5 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
                  only === value ? 'bg-muted text-foreground' : 'text-muted-foreground'
                }`}
              >
                {value === 'all' ? 'Everything' : 'Still open'}
              </button>
            ))}
          </div>
        </header>

        {/* Three numbers, and they are the three that matter. */}
        <dl className="mt-5 grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-border bg-border md:grid-cols-3">
          <div className="min-w-0 bg-card p-4">
            <dt className="flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
              <Banknote aria-hidden className="h-3.5 w-3.5 shrink-0" />
              Released this period
            </dt>
            <dd className="mt-1.5 break-words text-xl font-bold tracking-tight">{money(released)}</dd>
          </div>
          <div className="min-w-0 bg-card p-4">
            <dt className="flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
              <Clock aria-hidden className="h-3.5 w-3.5 shrink-0" />
              Past {promiseDays} days
            </dt>
            <dd className="mt-1.5 break-words text-xl font-bold tracking-tight text-destructive">
              {breaching.length}
            </dd>
          </div>
          <div className="min-w-0 bg-card p-4">
            <dt className="flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
              <AlertTriangle aria-hidden className="h-3.5 w-3.5 shrink-0" />
              Blocked on us
            </dt>
            <dd className="mt-1.5 break-words text-xl font-bold tracking-tight">{blocked.length}</dd>
          </div>
        </dl>

        {/* `relative`: sr-only is absolute, and a static scroller cannot clip it. */}
        <div className="relative mt-5 overflow-x-auto rounded-xl border border-border">
          <table className="w-full min-w-[52rem] border-collapse text-sm">
            <caption className="sr-only">
              Refunds, {sort.key === 'ageDays' ? 'by age' : 'by amount'}
            </caption>
            <thead>
              <tr className="border-b border-border bg-muted/50">
                <th scope="col" className="px-3 py-2.5 text-start font-semibold">
                  Refund
                </th>
                <th scope="col" className="px-3 py-2.5 text-start font-semibold">
                  Customer
                </th>
                <th scope="col" aria-sort={ariaSort('amount')} className="px-3 py-2.5 text-end font-semibold">
                  <button
                    type="button"
                    onClick={() => toggleSort('amount')}
                    className="inline-flex items-center gap-1 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                  >
                    Amount
                    <ArrowUpDown aria-hidden className="h-3 w-3" />
                  </button>
                </th>
                <th scope="col" aria-sort={ariaSort('ageDays')} className="px-3 py-2.5 text-start font-semibold">
                  <button
                    type="button"
                    onClick={() => toggleSort('ageDays')}
                    className="inline-flex items-center gap-1 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                  >
                    Age
                    <ArrowUpDown aria-hidden className="h-3 w-3" />
                  </button>
                </th>
                <th scope="col" className="px-3 py-2.5 text-start font-semibold">
                  Status
                </th>
              </tr>
            </thead>
            <tbody>
              {visible.map((row) => {
                const late = open(row) && row.ageDays > promiseDays
                return (
                  <tr key={row.id} className="border-b border-border/70 last:border-0">
                    <td className="px-3 py-3">
                      <a href="#" className="font-mono text-xs font-medium underline-offset-4 hover:underline">
                        {row.id}
                      </a>
                      <span className="block text-xs text-muted-foreground">{row.order}</span>
                    </td>
                    <td className="px-3 py-3">{row.customer}</td>
                    <td className="px-3 py-3 text-end font-medium">{money(row.amount)}</td>
                    <td className="px-3 py-3">
                      <span
                        className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold ${
                          late ? 'bg-destructive/10 text-destructive' : 'text-muted-foreground'
                        }`}
                      >
                        {row.ageDays} day{row.ageDays === 1 ? '' : 's'}
                        {late ? <span className="sr-only"> — past the promise</span> : null}
                      </span>
                      <span className="block text-xs text-muted-foreground">
                        raised {row.raisedOn}
                      </span>
                    </td>
                    <td className="px-3 py-3">
                      <span
                        className={`inline-block rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${STATUS_STYLE[row.status]}`}
                      >
                        {STATUS_LABEL[row.status]}
                      </span>
                      {/* Naming the blocker is what makes the queue workable. */}
                      {row.blocker ? (
                        <span className="mt-1 block max-w-[18rem] text-xs text-muted-foreground">
                          {row.blocker}
                        </span>
                      ) : null}
                    </td>
                  </tr>
                )
              })}
            </tbody>
          </table>
        </div>
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/refunds-overview-table.tsx and import it where you need the section:

import { RefundsOverviewTable } from '@/components/refunds-overview-table'

Customize

2 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.

Props

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.

PropTypeDefault
rowsRefundRow[]DEFAULT_ROWS
promiseDaysDays after which an open refund is breaching the promise.number5
currencystring'£'
classNamestring''

Not using React?

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.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

refunds-overview-table.html
<!--
  Refunds Overview Table — 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 is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section class="bg-background px-4 py-10 sm:px-6 ">
  <div class="mx-auto max-w-5xl">
    <header class="flex flex-wrap items-end justify-between gap-4">
      <div>
        <h2 class="text-xl font-bold tracking-tight">Refunds</h2>
        <p class="mt-1 text-sm text-muted-foreground">Oldest open first. A refund gets slower the longer nobody looks at it, so recency is the wrong default here.</p>
      </div>
      <div class="flex rounded-lg border border-border p-0.5">
        <button type="button" aria-pressed="true" class="rounded-md px-2.5 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring bg-muted text-foreground">Everything</button>
        <button type="button" aria-pressed="false" class="rounded-md px-2.5 py-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring text-muted-foreground">Still open</button>
      </div>
    </header>
    <dl class="mt-5 grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-border bg-border md:grid-cols-3">
      <div class="min-w-0 bg-card p-4">
        <dt class="flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide 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-banknote h-3.5 w-3.5 shrink-0" aria-hidden="true"><rect width="20" height="12" x="2" y="6" rx="2"></rect><circle cx="12" cy="12" r="2"></circle><path d="M6 12h.01M18 12h.01"></path></svg>Released this period</dt>
        <dd class="mt-1.5 break-words text-xl font-bold tracking-tight">£412.40</dd>
      </div>
      <div class="min-w-0 bg-card p-4">
        <dt class="flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide 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 shrink-0" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>Past 5 days</dt>
        <dd class="mt-1.5 break-words text-xl font-bold tracking-tight text-destructive">3</dd>
      </div>
      <div class="min-w-0 bg-card p-4">
        <dt class="flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide 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-triangle-alert h-3.5 w-3.5 shrink-0" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>Blocked on us</dt>
        <dd class="mt-1.5 break-words text-xl font-bold tracking-tight">3</dd>
      </div>
    </dl>
    <div class="relative mt-5 overflow-x-auto rounded-xl border border-border">
      <table class="w-full min-w-[52rem] border-collapse text-sm">
        <caption class="sr-only">Refunds, by age</caption>
        <thead>
          <tr class="border-b border-border bg-muted/50">
            <th scope="col" class="px-3 py-2.5 text-start font-semibold">Refund</th>
            <th scope="col" class="px-3 py-2.5 text-start font-semibold">Customer</th>
            <th scope="col" aria-sort="none" class="px-3 py-2.5 text-end font-semibold">
              <button type="button" class="inline-flex items-center gap-1 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Amount<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-arrow-up-down h-3 w-3" aria-hidden="true"><path d="m21 16-4 4-4-4"></path><path d="M17 20V4"></path><path d="m3 8 4-4 4 4"></path><path d="M7 4v16"></path></svg></button>
            </th>
            <th scope="col" aria-sort="descending" class="px-3 py-2.5 text-start font-semibold">
              <button type="button" class="inline-flex items-center gap-1 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Age<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-arrow-up-down h-3 w-3" aria-hidden="true"><path d="m21 16-4 4-4-4"></path><path d="M17 20V4"></path><path d="m3 8 4-4 4 4"></path><path d="M7 4v16"></path></svg></button>
            </th>
            <th scope="col" class="px-3 py-2.5 text-start font-semibold">Status</th>
          </tr>
        </thead>
        <tbody>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <a href="#" class="font-mono text-xs font-medium underline-offset-4 hover:underline">REF-20390</a>
              <span class="block text-xs text-muted-foreground">ORD-88104</span>
            </td>
            <td class="px-3 py-3">A. Ferreira</td>
            <td class="px-3 py-3 text-end font-medium">£214.00</td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold bg-destructive/10 text-destructive">13 days<span class="sr-only"> — past the promise</span></span>
              <span class="block text-xs text-muted-foreground">raised 31 Aug</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-amber-500/10 text-amber-700 dark:text-amber-400">Blocked</span>
              <span class="mt-1 block max-w-[18rem] text-xs text-muted-foreground">Bank details refused — customer contacted twice</span>
            </td>
          </tr>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <a href="#" class="font-mono text-xs font-medium underline-offset-4 hover:underline">REF-20402</a>
              <span class="block text-xs text-muted-foreground">ORD-88221</span>
            </td>
            <td class="px-3 py-3">Northwind Trading</td>
            <td class="px-3 py-3 text-end font-medium">£1,840.50</td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold bg-destructive/10 text-destructive">11 days<span class="sr-only"> — past the promise</span></span>
              <span class="block text-xs text-muted-foreground">raised 2 Sep</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-amber-500/10 text-amber-700 dark:text-amber-400">Blocked</span>
              <span class="mt-1 block max-w-[18rem] text-xs text-muted-foreground">Over the £1,000 approval limit — needs Rhea Patel</span>
            </td>
          </tr>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <a href="#" class="font-mono text-xs font-medium underline-offset-4 hover:underline">REF-20411</a>
              <span class="block text-xs text-muted-foreground">ORD-88377</span>
            </td>
            <td class="px-3 py-3">K. Ojo</td>
            <td class="px-3 py-3 text-end font-medium">£89.99</td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold bg-destructive/10 text-destructive">8 days<span class="sr-only"> — past the promise</span></span>
              <span class="block text-xs text-muted-foreground">raised 5 Sep</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-amber-500/10 text-amber-700 dark:text-amber-400">Blocked</span>
              <span class="mt-1 block max-w-[18rem] text-xs text-muted-foreground">Awaiting returned goods — carrier scan 4 days ago</span>
            </td>
          </tr>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <a href="#" class="font-mono text-xs font-medium underline-offset-4 hover:underline">REF-20418</a>
              <span class="block text-xs text-muted-foreground">ORD-88410</span>
            </td>
            <td class="px-3 py-3">M. Haugen</td>
            <td class="px-3 py-3 text-end font-medium">£928.00</td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold text-muted-foreground">4 days</span>
              <span class="block text-xs text-muted-foreground">raised 9 Sep</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-primary/10 text-primary">Approved</span>
            </td>
          </tr>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <a href="#" class="font-mono text-xs font-medium underline-offset-4 hover:underline">REF-20425</a>
              <span class="block text-xs text-muted-foreground">ORD-88502</span>
            </td>
            <td class="px-3 py-3">Contoso Logistics</td>
            <td class="px-3 py-3 text-end font-medium">£412.40</td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold text-muted-foreground">2 days</span>
              <span class="block text-xs text-muted-foreground">raised 11 Sep</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-emerald-500/10 text-emerald-700 dark:text-emerald-400">Released</span>
            </td>
          </tr>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <a href="#" class="font-mono text-xs font-medium underline-offset-4 hover:underline">REF-20431</a>
              <span class="block text-xs text-muted-foreground">ORD-88566</span>
            </td>
            <td class="px-3 py-3">S. Baptiste</td>
            <td class="px-3 py-3 text-end font-medium">£64.00</td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold text-muted-foreground">1 day</span>
              <span class="block text-xs text-muted-foreground">raised 12 Sep</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-block rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-muted text-muted-foreground">Rejected</span>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

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.

See the prompt

8 more blocks in After-Sale Service

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 Service

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More After-Sale Service blocks

View category
Open the full page for this block

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.

After-Sale Service259 lines1 dep
Open the full page for this block

Refund Request Form

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.

After-Sale Service348 lines1 dep
Open the full page for this block

Refund Status Tracker

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.

After-Sale Service241 lines1 dep
Open the full page for this block

Return or Exchange Picker

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.

After-Sale Service331 lines1 dep
Open the full page for this block

Return Label and Instructions

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.

After-Sale Service285 lines1 dep
Open the full page for this block

Service Repair Request

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.

After-Sale Service310 lines1 dep
Open the full page for this block

Review History

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.

After-Sale Service291 lines1 dep
Open the full page for this block

Gift Card Balance Check

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.

After-Sale Service254 lines1 dep