Invoice line items React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React invoice line items editor that works like a small spreadsheet, with VAT per rate computed the e-Factura way.
Type in a product cell to search the catalog by name, SKU or keyword; picking one fills its unit, price and VAT, and typing a new name adds it as free text with the price flagged until you enter one. Tab moves across the cells in reading order, Escape in a number cell puts back the value it had, and an empty cell shows its prompt only on the line you are in. Enter moves to the same cell of the next line; on the last line it walks right through the text cells and adds a line from the last one, and Cmd or Ctrl + Enter adds a line from anywhere in the table. Drag a line by its grip with the mouse, or focus the grip and press Space to lift, the arrows to move, Space to drop and Escape to cancel; Alt + Arrow Up / Down moves the line you are in, and Delete or Backspace on a focused grip removes it. The ⋯ menu duplicates, moves (⌥↑ / ⌥↓ work from any cell) and removes; a removed line leaves an undo row for five seconds, paused while you hover or focus it. The % / lei chip in a discount cell switches the discount type. Totals update on every keystroke: subtotal, discounts, the taxable base, VAT per rate and the grand total, which is announced once your edits settle. Narrower than 768px (or with layout cards) every line is a card: tap it to edit a copy in a bottom sheet (a dialog on desktop) and keep it with Save or Cmd / Ctrl + Enter, while closing the sheet discards the draft; long-press the grip to reorder, use ⋯ for Move up / Move down, and tap the Total row to open the breakdown.
Invoice line items is a Pro React component in the React form components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add invoice-line-items, together with Badge, Button, Combobox, Currency Input, Drawer and 14 more and the npm packages @dnd-kit/core, @dnd-kit/sortable, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add invoice-line-itemsInvoice line items is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import InvoiceLineItems from @/components/ui/invoice-line-items.
import { useState } from "react"
import {
InvoiceLineItems,
InvoiceTotals,
type InvoiceDocumentDiscount,
type InvoiceLine,
type InvoiceProduct,
} from "@/components/ui/invoice-line-items"
const PRODUCTS: InvoiceProduct[] = [
{ id: "crm-business", name: "Northwind CRM Business license", unit: "month", unitPrice: 250, vatRate: 0.21, sku: "CRM-BUS" },
{ id: "onboarding", name: "Setup and training", unit: "hour", unitPrice: 320, vatRate: 0.21 },
{ id: "guide", name: "Printed user guide", unit: "pc", unitPrice: 64.9, vatRate: 0.11 },
]
export function InvoiceEditor() {
const [lines, setLines] = useState<InvoiceLine[]>([
{ id: "l1", productId: "crm-business", product: "Northwind CRM Business license", quantity: 25, unit: "month", unitPrice: 250, vatRate: 0.21, discount: 10 },
])
const [discount, setDiscount] = useState<InvoiceDocumentDiscount | null>(null)
return (
<InvoiceLineItems
value={lines}
onValueChange={(next, totals) => {
setLines(next)
console.log("Amount due", totals.total)
}}
products={PRODUCTS}
units={["pc", "hour", "day", "month", "service"]}
documentDiscount={discount}
onDocumentDiscountChange={setDiscount}
name="invoice[lines]"
currency="RON"
locale="en-US"
labels={{ grandTotal: "Amount due" }}
/>
)
}
// the totals block alone (an invoice preview): pass lines or precomputed totals.
// DEFAULT_INVOICE_LINE_ITEMS_LABELS holds the English strings to spread under a translation
export function InvoicePreviewTotals({ lines }: { lines: InvoiceLine[] }) {
return <InvoiceTotals lines={lines} currency="RON" locale="en-US" />
}Props
valueInvoiceLine[]The lines (controlled). InvoiceLine is the lib/invoicing line { id, quantity, unitPrice, vatRate?, discount?, discountType? } plus product, productId, description, unit and vatCategory (tells the 0% rows apart: E exempt, AE reverse charge, K intra-community).
defaultValueInvoiceLine[]The starting lines while uncontrolled. Left out: one empty line (none when minLines is 0).
onValueChange(lines: InvoiceLine[], totals: InvoiceTotalsData) => voidEvery edit, with the totals from computeInvoiceTotals (net, VAT per rate, total).
productsdefault"[]"InvoiceProduct[]The product combobox's options: { id, name, description?, unit?, unitPrice, vatRate?, sku?, keywords? }. Name, SKU and keywords are searched; picking one fills the unit, price and VAT and keeps the quantity.
onSearchProducts(query: string) => Promise<InvoiceProduct[]>Async catalog search (the combobox's async mode, debounced, older answers dropped).
allowCustomProductsdefaulttruebooleanFree text in the product cell with an “Add …” row. A new product keeps its price empty and flagged until one is entered.
currencydefault"RON"stringISO 4217 code for prices and totals. Changing it never converts the amounts.
pricesIncludeVatdefaultfalsebooleanThe price column is the price with VAT (B2C quotes, shops); the totals end with “of which VAT”. Math per lib/invoicing either way.
vatRatesdefault"RO_VAT_RATES"VatRateOption[]The VAT select's options: { value, rate, category, label, description }. Default: 21%, 11%, 0% exempt, 0% reverse charge, 0% intra-community. A line with a rate not in the list shows it as text.
defaultVatRatedefault"0.21"numberThe VAT rate of new lines, as a fraction.
unitsdefault"["pcs", "hr", "day", "month", "kg", "m²", "service"]"string[]The unit list; any other unit can be typed.
showDescriptiondefaulttruebooleanA second line under the product for a description.
showUnitdefaulttruebooleanThe unit column.
showVatdefaulttruebooleanThe VAT column.
showDiscountdefaulttruebooleanThe discount column, with a % / amount switch per line.
defaultDiscountTypedefault"percent""percent" | "amount"The discount type of new lines.
documentDiscountdefault"null"{ type: "percent" | "amount"; value: number } | nullA discount on the whole document, shared across the lines by value before VAT.
onDocumentDiscountChange(discount: InvoiceDocumentDiscount | null) => voidMakes the totals' discount row editable.
showTotalsdefaulttruebooleanThe totals block under the lines.
totalsPositiondefault"below""below" | "end"below: a full-width block; end: a narrow block at the end under the table (always full width on phones).
layoutdefault"auto""auto" | "table" | "cards"auto: the spreadsheet table from 768px of the component's own width (not the screen's), cards with a sheet editor below it, so a narrow panel on a desktop gets the cards too. table and cards force one (the table scrolls inside its panel when it does not fit).
reorderabledefaulttruebooleanDrag handles (dnd-kit: mouse, a long press on touch, the keyboard), Alt + arrows and Move up / down in the menu.
minLinesdefault1numberLines that always stay: removing one of the last ones clears it instead. 0 shows an empty state when there are none.
maxLinesdefault0numberThe most lines; Add and Duplicate turn off at the limit. 0 means no limit.
errorsRecord<string, Partial<Record<"product" | "quantity" | "unitPrice" | "vatRate" | "discount", string>>>Server validation per line id and field: the cell turns invalid with the message in a tooltip (desktop) or under the field (the sheet).
readOnlydefaultfalsebooleanThe invoice as the customer sees it: a plain table without inputs, the same totals.
disableddefaultfalsebooleanEvery cell, handle and button is disabled.
sizedefault"md""sm" | "md"Cell controls 32 / 40px (44px on touch).
variantdefault"ghost""ghost" | "soft"ghost: a spreadsheet feel with a hover tint; soft: filled fields.
namedefault""stringA hidden input with the lines as JSON, for native forms.
localedefault""stringNumbers and money format with this locale; empty uses the UIProvider's, then the page's.
undoTimeoutdefault5000numberMilliseconds the undo row stays after a line is removed.
labelsdefault"DEFAULT_INVOICE_LINE_ITEMS_LABELS"Partial<InvoiceLineItemsLabels>Every string: the order, index and actions headers, column names (product, description, quantity, unit, unitPrice, unitPriceWithVat, vat, discount, total), actions (addLine, duplicate, moveUp, moveDown, remove, undo), the add row's lineCount(count, formatted), the empty state (empty, emptyDescription), the cells (descriptionPlaceholder, untitled), the pickers (productPlaceholder, createProduct(query), noProducts, unitPlaceholder, createUnit(query), priceMissing, newProduct, discountType, percent, amount), totals (gross, lineDiscounts, documentDiscount, net, vatRow(rate, base), grandTotal, includedVat(rate), showBreakdown, hideBreakdown), the sheet (editLine, saveLine, cancel, lineTitle(n), lineTotal), accessible names (caption, cellLabel(column, n), lineActions(n), reorder(n)), announcements (removed(n, product), moved(from, to), added(n), totalAnnouncement(total)) and the drag messages (pickedUp, movedTo, dropped, dragCancelled).
classNamesPartial<Record<InvoiceLineItemsSlot, string>>Classes for panel, table, head, row, handle, index, product, description, quantity, unit, price, vat, discount, total, actions, add, cards, card, sheet, empty, live and totals (the panel around InvoiceTotals).
Parts
InvoiceTotals
componentThe totals block on its own (an invoice preview or a sidebar): subtotal, document discount, VAT per rate and the total, from lines or precomputed totals.
linesInvoiceLine[]The lines to compute the totals from.
totalsInvoiceTotalsDataPrecomputed totals from computeInvoiceTotals; left out, they are computed from lines.
pricesIncludeVatdefaultfalsebooleanUnit prices already include VAT.
documentDiscountdefault"null"{ type: "percent" | "amount"; value: number } | nullA discount on the whole document, shown as its own row.
onDocumentDiscountChange(discount: InvoiceDocumentDiscount | null) => voidMakes the discount row editable.
currencydefault"RON"stringISO 4217 code for the amounts.
localestringIntl locale for the amounts; falls back to the UIProvider locale, then the browser.
collapsibledefaultfalseboolean | "auto"One Total row that opens the breakdown; auto does it while the invoice container is narrower than 768px.
readOnlydefaultfalsebooleanShows the totals without any editable row.
labelsPartial<InvoiceLineItemsLabels>Overrides for the totals' strings.
DEFAULT_INVOICE_LINE_ITEMS_LABELS
constantEvery English string of the editor and the totals, to spread under a translation and override a few keys.
InvoiceLine
typeOne line: id, quantity, unitPrice, vatRate, discount and discountType from lib/invoicing, plus product, productId, description, unit and vatCategory.
InvoiceProduct
typeA catalog entry for the product picker: { id, name, unitPrice, description?, unit?, vatRate?, sku?, keywords? }.
Examples
Quote with prices incl. VAT
pricesIncludeVat for B2C: the price column is “Price incl. VAT” and the VAT shows as an “incl. VAT” row under the total.
Read-only preview
The invoice as the customer sees it: plain text, the same totals, a 5% document discount.
Server errors
errors marks cells per line and field (a tooltip in the table, a line of text on the cards). A retired 9% rate stays visible as text.
Reverse charge in EUR
An EU client: VAT 0% with category AE (reverse charge) from vatRates, amounts in euro.
Compact table
layout table, size sm, soft cells, no description line, totals in a narrow block at the end.
Cards and the sheet editor
layout cards: tap a line to edit a copy (a bottom sheet on phones, a dialog on desktop) and keep it with Save line.
Credit note
Negative quantities for a storno: line values, VAT and the total keep their minus sign.
Async catalog search
onSearchProducts asks a server as you type; allowCustomProducts off keeps the lines to catalog products.
Dependencies
Files
- components/ui/invoice-line-items.tsx
FAQ
Is the React Invoice line items component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Invoice line items?
Run npx wingo-ui@latest add invoice-line-items. It also adds the Wingo UI items it builds on (Badge, Button, Combobox, Currency Input, Drawer and 14 more) and the npm packages @dnd-kit/core, @dnd-kit/sortable, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/invoice-line-items.
How does Invoice line items behave on phones?
Narrower than 768px (or with layout cards) every line is a card: tap it to edit a copy in a bottom sheet (a dialog on desktop) and keep it with Save or Cmd / Ctrl + Enter, while closing the sheet discards the draft; long-press the grip to reorder, use ⋯ for Move up / Move down, and tap the Total row to open the breakdown.
See also
Used in
More React form components
Articles using this component
- React Invoice Template: Line Items, VAT and Print View
Build it: screens and apps10 min read
- E-Factura VAT Calculation in TypeScript and React
Build it: screens and apps11 min read