Editable product and service rows — item, quantity, unit, price, discount, tax rate and a generic withholding field — with exact totals per tax rate. Money is decimal strings end to end (BigInt maths, half-up rounding), tax rules come from a locale pack (Turkish KDV and tevkifat ship in locale-data), and rows reorder by drag, keyboard or menu.
| Reorder | Description | Qty | Unit | PriceEUR | Discount | Tax | Amount | Actions |
|---|---|---|---|---|---|---|---|---|
| €2,900.00 | ||||||||
| €3,420.00 | ||||||||
| €6,120.00 |
- Subtotal
- €13,120.00
- Discounts
- −€680.00
- Net
- €12,440.00
- VAT 20%on €12,440.00
- €2,488.00
- Total
- €14,928.00
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { LineItems, trLineTaxPack, computeLineTotals } from "@vamos-ui/react/business";
<LineItems
currency="TRY"
taxPack={trLineTaxPack("2026-11-10")}
units={["hour", "day", "pcs"]}
value={rows}
onValueChange={(rows, totals) => setDraft({ rows, payable: totals.payable })}
/>
// the same maths on the server
const totals = computeLineTotals(rows, { taxPack: trLineTaxPack(), pricesIncludeTax: false });Keyboard: Enter on the last field of the last row adds a row; Alt+↑/↓ moves a row; Ctrl/⌘+Enter inserts below; Ctrl/⌘+Shift+Backspace deletes. The row menu offers the same actions without shortcuts or dragging.
Totals are announced politely (debounced) and every input is named "{column}, row {n}".
Examples
Turkish KDV and tevkifat
trLineTaxPack() reads the KDV rates in force on a date and the tevkifat codes. Withholding is a generic field (602 = 9/10); the summary shows KDV per rate, the withheld VAT and the payable amount, plus the amount in words.
| Reorder | Açıklama | Miktar | Birim | FiyatTRY | İskonto | KDV | Tevkifat | Tutar | Actions |
|---|---|---|---|---|---|---|---|---|---|
| ₺70.000,00 | |||||||||
| ₺6.500,00 | |||||||||
| ₺4.560,00 |
- Ara toplam
- ₺81.060,00
- Discounts
- ₺0,00
- Matrah
- ₺81.060,00
- KDV %20on ₺76.500,00
- ₺15.300,00
- KDV %10on ₺4.560,00
- ₺456,00
- Genel toplam
- ₺96.816,00
- KDV tevkifatı 602 · 9/10602 · 9/10 Etüt, plan-proje, danışmanlık, denetim ve benzeri hizmetler
- −₺12.600,00
- Ödenecek tutar
- ₺84.216,00
Tax-inclusive prices
Retail checkout: prices include VAT. The entered gross is kept exactly; the net base is derived per line.
| Reorder | Description | Qty | PriceGBP | Tax | Amount | Actions |
|---|---|---|---|---|---|---|
| £74.17 | ||||||
| £48.00 | ||||||
| £3.75 |
- Subtotal
- £125.92
- Net
- £125.92
- VAT 20%on £77.92
- £15.58
- VAT 0%on £48.00
- £0.00
- Total
- £141.50
Prices include tax
Row and document discounts
Each discount toggles between % and an amount. The document discount is allocated pro-rata to the lines (largest remainder), so the cents always add up.
| Reorder | Description | Qty | PriceUSD | Discount | Tax | Amount | Actions |
|---|---|---|---|---|---|---|---|
| $7,025.25 | |||||||
| $1,900.00 | |||||||
| $1,710.00 |
- Subtotal
- $12,900.00
- Discounts
- −$2,264.75
- Document discount
- Net
- $10,635.25
- VAT 0%on $10,635.25
- $0.00
- Total
- $10,635.25
Rows from a product catalogue
itemSource is a Record picker source; onItemPick copies name, price, unit and tax rate into the row.
| Reorder | Item | Description | Qty | PriceTRY | Tax | Amount | Actions |
|---|---|---|---|---|---|---|---|
| TRY 9,600.00 |
- Subtotal
- TRY 9,600.00
- VAT 20%on TRY 9,600.00
- TRY 1,920.00
- Total
- TRY 11,520.00
Read-only (printable)
readOnly renders plain text with row numbers — invoice previews, PDFs, customer portals.
| # | Description | Qty | Unit | PriceUSD | Tax | Amount |
|---|---|---|---|---|---|---|
| 1 | Brand identity refresh | 1 | project | $6,800.00 | 0% | $6,800.00 |
| 2 | Website design system (Figma) | 24 | hour | $120.00 | 0% | $2,880.00 |
| 3 | Stock photography licence | 8 | image | $45.00 | 0% | $360.00 |
- Subtotal
- $10,040.00
- VAT 0%on $10,040.00
- $0.00
- Total
- $10,040.00
Stacked on small screens
A container query stacks each row into a card below 40rem; header names become field labels.
| Reorder | Description | Qty | PriceEUR | Tax | Amount | Actions |
|---|---|---|---|---|---|---|
| €159.80 | ||||||
| €212.40 |
- Subtotal
- €372.20
- Net
- €372.20
- VAT 19%on €212.40
- €40.36
- VAT 7%on €159.80
- €11.19
- Total
- €423.75
API
LineItems
| Prop | Type | Default |
|---|---|---|
value / defaultValueRows. LineItem = { id, itemId?, description, quantity, unit?, unitPrice, discount?, taxRate?, withholding? } — every number is a decimal string. | LineItem[] | — |
onValueChangeCalled on every edit with the recalculated totals. | (rows: LineItem[], totals: LineTotals) => void | — |
currencyISO 4217 code for prices and totals. | string | LocaleProvider currency |
taxPackRates, withholding codes, rounding and the tax name. trLineTaxPack(date) builds the Turkish KDV pack from locale-data. | LineTaxPack | vatLineTaxPack |
taxRatesOverrides the pack's rates. | { value: string; label: string }[] | — |
pricesIncludeTaxUnit prices are gross; nets are derived. | boolean | false |
unitsUnit choices (a Select). Without it the unit cell is free text. | string[] | — |
itemSourceProducts / services for the Item column. | RecordSource<R> | — |
onItemPickFields copied into the row when a record is picked. | (record: R) => Partial<LineItem> | — |
columnsVisible columns in order. Default: all that apply (item with itemSource, withholding when the pack has codes). | ("item" | "description" | "quantity" | "unit" | "unitPrice" | "discount" | "tax" | "withholding" | "total")[] | — |
documentDiscountDiscount on the whole document, allocated pro-rata. | { type: "percent" | "amount"; value: string } | — |
onDocumentDiscountChangeMakes the document discount editable in the summary. | (d?: LineDiscount) => void | — |
readOnlyPrintable summary: no inputs, numbered rows. | boolean | false |
disabledShows values but blocks editing. | boolean | false |
nameHidden inputs for native form posts: name[0][quantity], name[0][discount][type]… | string | — |
minRowsRows that can't be deleted. | number | 1 |
newRowDefaults for added rows. | () => Partial<LineItem> | — |
hideSummaryHide the totals block (render LineItemsSummary elsewhere). | boolean | false |
summaryFooterExtra content under the totals (amount in words, notes). | ReactNode | — |
labelsBuilt-in strings (lineItems namespace). | Partial<LineItemsLabels> | — |
LineItemsSummary
| Prop | Type | Default |
|---|---|---|
totalsFrom computeLineTotals or onValueChange. | LineTotals | — |
currency / taxPack / pricesIncludeTaxAs on LineItems. | string / LineTaxPack / boolean | — |
documentDiscount / onDocumentDiscountChangeEditable document discount row. | LineDiscount / fn | — |
computeLineTotals(rows, options)
| Prop | Type | Default |
|---|---|---|
options.taxPackRounding: "line" (per line, tr e-invoice) or "document" (once per rate). | LineTaxPack | — |
options.pricesIncludeTaxGross entry. | boolean | — |
options.documentDiscountAllocated pro-rata to line nets. | LineDiscount | — |
returnslines[], subtotal, discount, net, byRate[], withholding[], tax, total, withheld, payable, invalidCount — all decimal strings. | LineTotals | — |