Web InnoventixFreeCode

Invoice Table

Original · free

invoice line-items table

byWeb InnoventixReact + Tailwind
tableinvoicetables
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/table-invoice.json
table-invoice.tsx
"use client"

import { useId, useMemo, useRef, useState } from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"

type LineItem = {
  id: string
  description: string
  detail: string
  quantity: number
  unitPrice: number
}

type Currency = {
  code: string
  symbol: string
  label: string
}

const CURRENCIES: Currency[] = [
  { code: "USD", symbol: "$", label: "USD — US Dollar" },
  { code: "EUR", symbol: "€", label: "EUR — Euro" },
  { code: "GBP", symbol: "£", label: "GBP — Pound Sterling" },
  { code: "AUD", symbol: "$", label: "AUD — Australian Dollar" },
]

const INITIAL_ITEMS: LineItem[] = [
  {
    id: "li-1",
    description: "Brand identity system",
    detail: "Logotype, color, type scale, and usage guidelines",
    quantity: 1,
    unitPrice: 4800,
  },
  {
    id: "li-2",
    description: "Marketing site design",
    detail: "Six responsive page layouts, delivered in Figma",
    quantity: 6,
    unitPrice: 620,
  },
  {
    id: "li-3",
    description: "Front-end build",
    detail: "Component library + page implementation, per day",
    quantity: 9,
    unitPrice: 540,
  },
  {
    id: "li-4",
    description: "Launch copywriting",
    detail: "Homepage, product tour, and two email sequences",
    quantity: 1,
    unitPrice: 1250,
  },
]

function toNumber(raw: string): number {
  const parsed = Number.parseFloat(raw.replace(/[^0-9.]/g, ""))
  return Number.isFinite(parsed) ? parsed : 0
}

function IconPlus({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" aria-hidden="true" className={className}>
      <path d="M12 5v14M5 12h14" />
    </svg>
  )
}

function IconMinus({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" aria-hidden="true" className={className}>
      <path d="M5 12h14" />
    </svg>
  )
}

function IconTrash({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M4 7h16M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2m2 0-.7 12a2 2 0 0 1-2 1.9H8.7a2 2 0 0 1-2-1.9L6 7" />
      <path d="M10 11v6M14 11v6" />
    </svg>
  )
}

function IconCheck({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.4} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M20 6 9 17l-5-5" />
    </svg>
  )
}

function IconReceipt({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.7} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={className}>
      <path d="M5 3v18l2-1.4L9 21l2-1.4L13 21l2-1.4L17 21l2-1.4V3l-2 1.4L15 3l-2 1.4L11 3 9 4.4 7 3 5 4.4Z" />
      <path d="M8.5 9h7M8.5 13h5" />
    </svg>
  )
}

export default function TableInvoice() {
  const reduce = useReducedMotion()
  const uid = useId()
  const idCounter = useRef(INITIAL_ITEMS.length)

  const [items, setItems] = useState<LineItem[]>(INITIAL_ITEMS)
  const [currencyCode, setCurrencyCode] = useState<string>("USD")
  const [discountPercent, setDiscountPercent] = useState<number>(10)
  const [taxPercent, setTaxPercent] = useState<number>(8.25)
  const [dueDate, setDueDate] = useState<string>("2026-08-14")
  const [paid, setPaid] = useState<boolean>(false)

  const currency = CURRENCIES.find((c) => c.code === currencyCode) ?? CURRENCIES[0]

  const formatter = useMemo(
    () => new Intl.NumberFormat("en-US", { style: "currency", currency: currency.code }),
    [currency.code],
  )
  const money = (value: number) => formatter.format(value)

  const subtotal = items.reduce((sum, it) => sum + it.quantity * it.unitPrice, 0)
  const discountAmount = subtotal * (discountPercent / 100)
  const taxable = Math.max(0, subtotal - discountAmount)
  const taxAmount = taxable * (taxPercent / 100)
  const total = taxable + taxAmount

  function updateItem(id: string, patch: Partial<LineItem>) {
    setItems((prev) => prev.map((it) => (it.id === id ? { ...it, ...patch } : it)))
  }

  function stepQuantity(id: string, delta: number) {
    setItems((prev) =>
      prev.map((it) => (it.id === id ? { ...it, quantity: Math.max(0, it.quantity + delta) } : it)),
    )
  }

  function removeItem(id: string) {
    setItems((prev) => prev.filter((it) => it.id !== id))
  }

  function addItem() {
    idCounter.current += 1
    setItems((prev) => [
      ...prev,
      {
        id: `li-${idCounter.current}`,
        description: "New line item",
        detail: "Describe the deliverable",
        quantity: 1,
        unitPrice: 0,
      },
    ])
  }

  const rowMotion = reduce
    ? {}
    : {
        initial: { opacity: 0, y: -6 },
        animate: { opacity: 1, y: 0 },
        exit: { opacity: 0, y: -6, transition: { duration: 0.16 } },
        transition: { duration: 0.24, ease: [0.22, 1, 0.36, 1] as const },
      }

  const inputBase =
    "w-full rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-slate-900 placeholder:text-slate-400 outline-none transition focus-visible:border-indigo-400 focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500 dark:focus-visible:border-indigo-500"

  const focusRing =
    "outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950"

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-24 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes invtbl-sheen {
          0% { background-position: -140% 0; }
          100% { background-position: 240% 0; }
        }
        @keyframes invtbl-rise {
          0% { opacity: 0; transform: translateY(10px); }
          100% { opacity: 1; transform: translateY(0); }
        }
        .invtbl-rise { animation: invtbl-rise 0.5s cubic-bezier(0.22,1,0.36,1) both; }
        .invtbl-sheen {
          background-image: linear-gradient(110deg, transparent 30%, rgba(255,255,255,0.55) 50%, transparent 70%);
          background-size: 220% 100%;
          animation: invtbl-sheen 3.6s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .invtbl-rise, .invtbl-sheen { animation: none !important; }
        }
      `}</style>

      {/* Ambient backdrop */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        <div className="absolute -top-24 left-1/2 h-72 w-[42rem] -translate-x-1/2 rounded-full bg-indigo-300/25 blur-3xl dark:bg-indigo-600/15" />
        <div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,rgba(15,23,42,0.05)_1px,transparent_0)] bg-[size:22px_22px] dark:bg-[radial-gradient(circle_at_1px_1px,rgba(148,163,184,0.08)_1px,transparent_0)]" />
      </div>

      <div className="invtbl-rise relative mx-auto w-full max-w-5xl">
        <div className="overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-[0_24px_60px_-24px_rgba(30,27,75,0.35)] dark:border-slate-800 dark:bg-slate-900 dark:shadow-[0_24px_60px_-24px_rgba(0,0,0,0.7)]">
          {/* Header */}
          <header className="relative border-b border-slate-200 bg-gradient-to-br from-indigo-600 via-violet-600 to-indigo-700 px-6 py-8 text-white sm:px-8 dark:border-slate-800">
            <div className="pointer-events-none absolute inset-0 opacity-40 [background:radial-gradient(60%_100%_at_100%_0%,rgba(255,255,255,0.25),transparent)]" />
            <div className="relative flex flex-wrap items-start justify-between gap-6">
              <div className="flex items-start gap-4">
                <span className="grid size-12 shrink-0 place-items-center rounded-xl bg-white/15 ring-1 ring-white/25 backdrop-blur">
                  <IconReceipt className="size-6 text-white" />
                </span>
                <div>
                  <p className="text-xs font-semibold uppercase tracking-[0.22em] text-indigo-100">Invoice</p>
                  <p className="mt-0.5 text-2xl font-semibold tracking-tight">Halden &amp; Rowe Studio</p>
                  <p className="mt-1 text-sm text-indigo-100/90">hello@haldenrowe.studio · +1 (415) 555-0139</p>
                </div>
              </div>

              <div className="text-right">
                <p className="text-xs font-semibold uppercase tracking-[0.22em] text-indigo-100">No.</p>
                <p className="font-mono text-lg font-semibold tracking-tight">HR-2026-0412</p>
                <span
                  className={`mt-2 inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wide ring-1 ${
                    paid
                      ? "bg-emerald-400/20 text-emerald-50 ring-emerald-200/50"
                      : "bg-amber-400/20 text-amber-50 ring-amber-200/50"
                  }`}
                >
                  <span className={`size-1.5 rounded-full ${paid ? "bg-emerald-300" : "bg-amber-300"}`} />
                  {paid ? "Paid" : "Due"}
                </span>
              </div>
            </div>

            <div className="relative mt-8 grid gap-6 sm:grid-cols-3">
              <div>
                <p className="text-[0.68rem] font-semibold uppercase tracking-[0.2em] text-indigo-100/80">Billed to</p>
                <p className="mt-1.5 text-sm font-medium leading-relaxed text-white">
                  Cascade Coffee Roasters
                  <br />
                  <span className="text-indigo-100/90">411 Marlow Ave, Portland, OR 97204</span>
                </p>
              </div>
              <div>
                <p className="text-[0.68rem] font-semibold uppercase tracking-[0.2em] text-indigo-100/80">Issued</p>
                <p className="mt-1.5 text-sm font-medium text-white">July 16, 2026</p>
              </div>
              <div>
                <label htmlFor={`${uid}-due`} className="text-[0.68rem] font-semibold uppercase tracking-[0.2em] text-indigo-100/80">
                  Payment due
                </label>
                <input
                  id={`${uid}-due`}
                  type="date"
                  value={dueDate}
                  onChange={(e) => setDueDate(e.target.value)}
                  className={`mt-1.5 w-full rounded-lg border border-white/25 bg-white/10 px-2.5 py-1.5 text-sm font-medium text-white [color-scheme:dark] placeholder:text-indigo-100 ${focusRing} focus-visible:ring-offset-indigo-700`}
                />
              </div>
            </div>
          </header>

          {/* Line-items table */}
          <div className="px-2 py-2 sm:px-4 sm:py-4">
            <div className="overflow-x-auto">
              <table className="w-full min-w-[640px] border-separate border-spacing-0 text-sm">
                <caption className="sr-only">Editable invoice line items with quantity, unit price, and amount.</caption>
                <thead>
                  <tr className="text-left text-[0.68rem] uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
                    <th scope="col" className="px-3 py-3 font-semibold">Description</th>
                    <th scope="col" className="px-3 py-3 text-center font-semibold">Qty</th>
                    <th scope="col" className="px-3 py-3 text-right font-semibold">Unit price</th>
                    <th scope="col" className="px-3 py-3 text-right font-semibold">Amount</th>
                    <th scope="col" className="px-3 py-3">
                      <span className="sr-only">Remove</span>
                    </th>
                  </tr>
                </thead>
                <tbody>
                  <AnimatePresence initial={false}>
                    {items.map((item) => {
                      const amount = item.quantity * item.unitPrice
                      return (
                        <motion.tr
                          key={item.id}
                          layout={!reduce}
                          {...rowMotion}
                          className="group align-top"
                        >
                          {/* Description */}
                          <td className="border-t border-slate-100 px-3 py-3 dark:border-slate-800">
                            <input
                              aria-label="Line item description"
                              value={item.description}
                              onChange={(e) => updateItem(item.id, { description: e.target.value })}
                              className={`${inputBase} font-medium`}
                            />
                            <input
                              aria-label="Line item detail"
                              value={item.detail}
                              onChange={(e) => updateItem(item.id, { detail: e.target.value })}
                              className={`${inputBase} mt-1.5 text-xs text-slate-500 dark:text-slate-400`}
                            />
                          </td>

                          {/* Quantity stepper */}
                          <td className="border-t border-slate-100 px-3 py-3 dark:border-slate-800">
                            <div className="mx-auto flex w-[7.5rem] items-center rounded-lg border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900">
                              <button
                                type="button"
                                aria-label={`Decrease quantity for ${item.description}`}
                                onClick={() => stepQuantity(item.id, -1)}
                                className={`grid size-8 shrink-0 place-items-center rounded-l-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 disabled:opacity-40 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white ${focusRing}`}
                                disabled={item.quantity <= 0}
                              >
                                <IconMinus className="size-3.5" />
                              </button>
                              <input
                                aria-label={`Quantity for ${item.description}`}
                                type="number"
                                min={0}
                                inputMode="numeric"
                                value={item.quantity}
                                onChange={(e) => updateItem(item.id, { quantity: Math.max(0, Math.round(toNumber(e.target.value))) })}
                                className={`w-full min-w-0 border-x border-slate-200 bg-transparent py-1.5 text-center font-mono text-sm tabular-nums text-slate-900 outline-none [appearance:textfield] focus-visible:bg-indigo-50 dark:border-slate-700 dark:text-slate-100 dark:focus-visible:bg-slate-800 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none`}
                              />
                              <button
                                type="button"
                                aria-label={`Increase quantity for ${item.description}`}
                                onClick={() => stepQuantity(item.id, 1)}
                                className={`grid size-8 shrink-0 place-items-center rounded-r-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white ${focusRing}`}
                              >
                                <IconPlus className="size-3.5" />
                              </button>
                            </div>
                          </td>

                          {/* Unit price */}
                          <td className="border-t border-slate-100 px-3 py-3 dark:border-slate-800">
                            <div className="relative ml-auto w-[8.5rem]">
                              <span className="pointer-events-none absolute inset-y-0 left-2.5 grid place-items-center font-mono text-sm text-slate-400 dark:text-slate-500">
                                {currency.symbol}
                              </span>
                              <input
                                aria-label={`Unit price for ${item.description}`}
                                type="number"
                                min={0}
                                step={0.01}
                                inputMode="decimal"
                                value={item.unitPrice}
                                onChange={(e) => updateItem(item.id, { unitPrice: Math.max(0, toNumber(e.target.value)) })}
                                className={`${inputBase} pl-6 text-right font-mono tabular-nums [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none`}
                              />
                            </div>
                          </td>

                          {/* Amount */}
                          <td className="border-t border-slate-100 px-3 py-3 text-right align-middle dark:border-slate-800">
                            <span className="font-mono text-sm font-semibold tabular-nums text-slate-900 dark:text-slate-100">
                              {money(amount)}
                            </span>
                          </td>

                          {/* Remove */}
                          <td className="border-t border-slate-100 px-3 py-3 text-right align-middle dark:border-slate-800">
                            <button
                              type="button"
                              aria-label={`Remove ${item.description}`}
                              onClick={() => removeItem(item.id)}
                              className={`grid size-8 place-items-center rounded-lg text-slate-400 opacity-70 transition hover:bg-rose-50 hover:text-rose-600 hover:opacity-100 group-hover:opacity-100 dark:text-slate-500 dark:hover:bg-rose-950/50 dark:hover:text-rose-400 ${focusRing}`}
                            >
                              <IconTrash className="size-4" />
                            </button>
                          </td>
                        </motion.tr>
                      )
                    })}
                  </AnimatePresence>

                  {items.length === 0 && (
                    <tr>
                      <td colSpan={5} className="border-t border-slate-100 px-3 py-10 text-center text-sm text-slate-500 dark:border-slate-800 dark:text-slate-400">
                        No line items yet. Add your first deliverable below.
                      </td>
                    </tr>
                  )}
                </tbody>
              </table>
            </div>

            <div className="px-1 pb-1 pt-3">
              <button
                type="button"
                onClick={addItem}
                className={`inline-flex items-center gap-2 rounded-xl border border-dashed border-slate-300 px-3.5 py-2 text-sm font-medium text-slate-600 transition hover:border-indigo-400 hover:bg-indigo-50 hover:text-indigo-700 dark:border-slate-700 dark:text-slate-300 dark:hover:border-indigo-500 dark:hover:bg-indigo-950/40 dark:hover:text-indigo-300 ${focusRing}`}
              >
                <IconPlus className="size-4" />
                Add line item
              </button>
            </div>
          </div>

          {/* Footer: terms + totals */}
          <div className="grid gap-8 border-t border-slate-200 px-6 py-8 sm:px-8 md:grid-cols-2 dark:border-slate-800">
            <div className="space-y-5">
              <div>
                <h3 className="text-xs font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">Notes &amp; terms</h3>
                <p className="mt-2 max-w-sm text-sm leading-relaxed text-slate-600 dark:text-slate-300">
                  Payment is due within 30 days of the issue date. A 1.5% monthly service charge applies to balances past due. Bank transfer and card accepted.
                </p>
              </div>

              <div className="flex flex-wrap items-center gap-3">
                <label htmlFor={`${uid}-currency`} className="text-xs font-semibold uppercase tracking-[0.16em] text-slate-500 dark:text-slate-400">
                  Currency
                </label>
                <select
                  id={`${uid}-currency`}
                  value={currencyCode}
                  onChange={(e) => setCurrencyCode(e.target.value)}
                  className={`rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-800 transition dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 ${focusRing}`}
                >
                  {CURRENCIES.map((c) => (
                    <option key={c.code} value={c.code}>
                      {c.label}
                    </option>
                  ))}
                </select>
              </div>

              <button
                type="button"
                role="switch"
                aria-checked={paid}
                onClick={() => setPaid((p) => !p)}
                className={`inline-flex items-center gap-3 rounded-full py-1 ${focusRing}`}
              >
                <span
                  className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${
                    paid ? "bg-emerald-500" : "bg-slate-300 dark:bg-slate-700"
                  }`}
                >
                  <span
                    className={`inline-flex size-5 translate-x-0.5 items-center justify-center rounded-full bg-white shadow transition-transform ${
                      paid ? "translate-x-[1.375rem]" : ""
                    }`}
                  >
                    {paid && <IconCheck className="size-3 text-emerald-600" />}
                  </span>
                </span>
                <span className="text-sm font-medium text-slate-700 dark:text-slate-200">
                  {paid ? "Marked as paid" : "Mark invoice as paid"}
                </span>
              </button>
            </div>

            {/* Totals */}
            <div className="rounded-2xl border border-slate-200 bg-slate-50/70 p-5 dark:border-slate-800 dark:bg-slate-950/40">
              <dl className="space-y-3 text-sm">
                <div className="flex items-center justify-between">
                  <dt className="text-slate-600 dark:text-slate-300">Subtotal</dt>
                  <dd className="font-mono tabular-nums text-slate-900 dark:text-slate-100">{money(subtotal)}</dd>
                </div>

                <div className="flex items-center justify-between gap-3">
                  <dt className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
                    <span>Discount</span>
                    <span className="inline-flex items-center rounded-md border border-slate-200 bg-white px-1.5 dark:border-slate-700 dark:bg-slate-900">
                      <label htmlFor={`${uid}-discount`} className="sr-only">Discount percentage</label>
                      <input
                        id={`${uid}-discount`}
                        type="number"
                        min={0}
                        max={100}
                        step={0.5}
                        inputMode="decimal"
                        value={discountPercent}
                        onChange={(e) => setDiscountPercent(Math.min(100, Math.max(0, toNumber(e.target.value))))}
                        className={`w-11 bg-transparent py-0.5 text-right font-mono text-xs tabular-nums text-slate-800 outline-none [appearance:textfield] dark:text-slate-100 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none ${focusRing} rounded`}
                      />
                      <span className="pl-0.5 text-xs text-slate-400 dark:text-slate-500">%</span>
                    </span>
                  </dt>
                  <dd className="font-mono tabular-nums text-rose-600 dark:text-rose-400">
                    {discountAmount > 0 ? `- ${money(discountAmount)}` : money(0)}
                  </dd>
                </div>

                <div className="flex items-center justify-between gap-3">
                  <dt className="flex items-center gap-2 text-slate-600 dark:text-slate-300">
                    <span>Tax</span>
                    <span className="inline-flex items-center rounded-md border border-slate-200 bg-white px-1.5 dark:border-slate-700 dark:bg-slate-900">
                      <label htmlFor={`${uid}-tax`} className="sr-only">Tax percentage</label>
                      <input
                        id={`${uid}-tax`}
                        type="number"
                        min={0}
                        max={100}
                        step={0.05}
                        inputMode="decimal"
                        value={taxPercent}
                        onChange={(e) => setTaxPercent(Math.min(100, Math.max(0, toNumber(e.target.value))))}
                        className={`w-12 bg-transparent py-0.5 text-right font-mono text-xs tabular-nums text-slate-800 outline-none [appearance:textfield] dark:text-slate-100 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none ${focusRing} rounded`}
                      />
                      <span className="pl-0.5 text-xs text-slate-400 dark:text-slate-500">%</span>
                    </span>
                  </dt>
                  <dd className="font-mono tabular-nums text-slate-900 dark:text-slate-100">{money(taxAmount)}</dd>
                </div>

                <div className="mt-2 border-t border-dashed border-slate-300 pt-4 dark:border-slate-700">
                  <div className="flex items-end justify-between">
                    <dt className="text-sm font-semibold uppercase tracking-wide text-slate-700 dark:text-slate-200">
                      Total due
                    </dt>
                    <dd
                      aria-live="polite"
                      className="font-mono text-2xl font-bold tabular-nums text-slate-900 dark:text-white"
                    >
                      {money(total)}
                    </dd>
                  </div>
                  <p className="mt-1 text-right text-xs text-slate-500 dark:text-slate-400">
                    {items.length} item{items.length === 1 ? "" : "s"} · {currency.code}
                  </p>
                </div>
              </dl>

              <button
                type="button"
                className={`relative mt-5 w-full overflow-hidden rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 transition hover:from-indigo-500 hover:to-violet-500 ${focusRing}`}
              >
                <span aria-hidden="true" className="invtbl-sheen pointer-events-none absolute inset-0" />
                <span className="relative">Send invoice · {money(total)}</span>
              </button>
            </div>
          </div>
        </div>

        <p className="mt-4 text-center text-xs text-slate-400 dark:text-slate-500">
          Every field is editable — adjust quantities, prices, discount, and tax to watch the total recalculate live.
        </p>
      </div>
    </section>
  )
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →