ComponentsForms & inputs

Line items

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.

Sortable (dnd-kit) · Record picker · Currency / Number input business/line-items.tsx, business/line-items-core.tstheme · material · accent · density · font
Quote Q-2026-114 lines. Enter on the last field adds a row · Alt+↑/↓ moves a row · Ctrl/⌘+Shift+Backspace deletes it
ReorderDescriptionQtyUnitPriceEURDiscountTaxAmountActions
€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

$ pnpm add @vamos-ui/react

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.

Fatura kalemleri. Enter on the last field adds a row · Alt+↑/↓ moves a row · Ctrl/⌘+Shift+Backspace deletes it
ReorderAçıklamaMiktarBirimFiyatTRYİskontoKDVTevkifatTutarActions
₺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.

Order lines. Enter on the last field adds a row · Alt+↑/↓ moves a row · Ctrl/⌘+Shift+Backspace deletes it
ReorderDescriptionQtyPriceGBPTaxAmountActions
£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.

Annual plan quote. Enter on the last field adds a row · Alt+↑/↓ moves a row · Ctrl/⌘+Shift+Backspace deletes it
ReorderDescriptionQtyPriceUSDDiscountTaxAmountActions
$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.

Order lines. Enter on the last field adds a row · Alt+↑/↓ moves a row · Ctrl/⌘+Shift+Backspace deletes it
ReorderItemDescriptionQtyPriceTRYTaxAmountActions
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.

Invoice INV-2026-0418
#DescriptionQtyUnitPriceUSDTaxAmount
1Brand identity refresh1project$6,800.000%$6,800.00
2Website design system (Figma)24hour$120.000%$2,880.00
3Stock photography licence8image$45.000%$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.

Expense claim. Enter on the last field adds a row · Alt+↑/↓ moves a row · Ctrl/⌘+Shift+Backspace deletes it
ReorderDescriptionQtyPriceEURTaxAmountActions
€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

PropTypeDefault
value / defaultValue
Rows. LineItem = { id, itemId?, description, quantity, unit?, unitPrice, discount?, taxRate?, withholding? } — every number is a decimal string.
LineItem[]—
onValueChange
Called on every edit with the recalculated totals.
(rows: LineItem[], totals: LineTotals) => void—
currency
ISO 4217 code for prices and totals.
stringLocaleProvider currency
taxPack
Rates, withholding codes, rounding and the tax name. trLineTaxPack(date) builds the Turkish KDV pack from locale-data.
LineTaxPackvatLineTaxPack
taxRates
Overrides the pack's rates.
{ value: string; label: string }[]—
pricesIncludeTax
Unit prices are gross; nets are derived.
booleanfalse
units
Unit choices (a Select). Without it the unit cell is free text.
string[]—
itemSource
Products / services for the Item column.
RecordSource<R>—
onItemPick
Fields copied into the row when a record is picked.
(record: R) => Partial<LineItem>—
columns
Visible 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")[]—
documentDiscount
Discount on the whole document, allocated pro-rata.
{ type: "percent" | "amount"; value: string }—
onDocumentDiscountChange
Makes the document discount editable in the summary.
(d?: LineDiscount) => void—
readOnly
Printable summary: no inputs, numbered rows.
booleanfalse
disabled
Shows values but blocks editing.
booleanfalse
name
Hidden inputs for native form posts: name[0][quantity], name[0][discount][type]…
string—
minRows
Rows that can't be deleted.
number1
newRow
Defaults for added rows.
() => Partial<LineItem>—
hideSummary
Hide the totals block (render LineItemsSummary elsewhere).
booleanfalse
summaryFooter
Extra content under the totals (amount in words, notes).
ReactNode—
labels
Built-in strings (lineItems namespace).
Partial<LineItemsLabels>—

LineItemsSummary

PropTypeDefault
totals
From computeLineTotals or onValueChange.
LineTotals—
currency / taxPack / pricesIncludeTax
As on LineItems.
string / LineTaxPack / boolean—
documentDiscount / onDocumentDiscountChange
Editable document discount row.
LineDiscount / fn—

computeLineTotals(rows, options)

PropTypeDefault
options.taxPack
Rounding: "line" (per line, tr e-invoice) or "document" (once per rate).
LineTaxPack—
options.pricesIncludeTax
Gross entry.
boolean—
options.documentDiscount
Allocated pro-rata to line nets.
LineDiscount—
returns
lines[], subtotal, discount, net, byRate[], withholding[], tax, total, withheld, payable, invalidCount — all decimal strings.
LineTotals—