Open demo

CSS code

HTML
<div><span class="lbl">Invoice builder with editable line items</span>
<form class="ib" id="ib1" data-builder aria-labelledby="ib1-title" novalidate>
  <div class="ib-head">
    <div><h2 class="ib-title" id="ib1-title">New invoice</h2><p class="ib-sub">Mossgrove Web Studio · draft, not sent</p></div>
    <button class="ib-btn" type="submit">Save draft</button>
  </div>
  <fieldset class="ib-fs"><legend class="ib-legend">Details</legend>
    <div class="ib-grid">
      <div class="ib-field"><label class="ib-label" for="ib1-no">Invoice number</label><input class="ib-input" id="ib1-no" type="text" value="MW-2026-031"></div>
      <div class="ib-field"><label class="ib-label" for="ib1-issued">Issue date</label><input class="ib-input" id="ib1-issued" type="date" value="2026-10-19"></div>
      <div class="ib-field"><label class="ib-label" for="ib1-due">Due date</label><input class="ib-input" id="ib1-due" type="date" value="2026-11-18"></div>
      <div class="ib-field"><label class="ib-label" for="ib1-cur">Currency</label><select class="ib-input ib-select" id="ib1-cur" data-currency><option value="USD" data-locale="en-US" selected>US dollar (USD)</option><option value="EUR" data-locale="de-DE">Euro (EUR)</option><option value="GBP" data-locale="en-GB">Pound sterling (GBP)</option><option value="JPY" data-locale="ja-JP">Japanese yen (JPY)</option><option value="CAD" data-locale="en-CA">Canadian dollar (CAD)</option></select></div>
      <div class="ib-field ib-field--2"><label class="ib-label" for="ib1-client">Bill to</label><input class="ib-input" id="ib1-client" type="text" value="Kettle & Crane Tea Rooms" autocomplete="organization"></div>
      <div class="ib-field ib-field--2"><label class="ib-label" for="ib1-email">Client email</label><input class="ib-input" id="ib1-email" type="email" value="[email protected]" autocomplete="email"></div>
    </div>
  </fieldset>
  <div class="ib-scroll">
  <table class="ib-table">
    <caption class="ib-caption">Line items</caption>
    <thead><tr class=""><th scope="col" class=" ">Description</th><th scope="col" class=" num">Qty</th><th scope="col" class=" num">Unit price</th><th scope="col" class=" num">Amount</th><th scope="col" class=" ib-rmcol"><span class="sr-only">Remove</span></th></tr></thead>
    <tbody data-lines><tr data-line class=""><td class=""><label class="sr-only" for="ib1-desc-1">Description, line <span data-n>1</span></label><input class="ib-input" type="text" data-field="desc" id="ib1-desc-1" value="Discovery workshop"></td><td class=""><label class="sr-only" for="ib1-qty-1">Quantity, line <span data-n>1</span></label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib1-qty-1" value="1"></td><td class=""><label class="sr-only" for="ib1-price-1">Unit price, line <span data-n>1</span></label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib1-price-1" value="1200"></td><td class=" num ib-amt" data-amount>$1,200.00</td><td class=" ib-rmcell"><button class="ib-rm" type="button" data-remove aria-label="Remove line 1" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr><tr data-line class=""><td class=""><label class="sr-only" for="ib1-desc-2">Description, line <span data-n>2</span></label><input class="ib-input" type="text" data-field="desc" id="ib1-desc-2" value="Page templates"></td><td class=""><label class="sr-only" for="ib1-qty-2">Quantity, line <span data-n>2</span></label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib1-qty-2" value="6"></td><td class=""><label class="sr-only" for="ib1-price-2">Unit price, line <span data-n>2</span></label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib1-price-2" value="340"></td><td class=" num ib-amt" data-amount>$2,040.00</td><td class=" ib-rmcell"><button class="ib-rm" type="button" data-remove aria-label="Remove line 2" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr><tr data-line class=""><td class=""><label class="sr-only" for="ib1-desc-3">Description, line <span data-n>3</span></label><input class="ib-input" type="text" data-field="desc" id="ib1-desc-3" value="Copywriting, per page"></td><td class=""><label class="sr-only" for="ib1-qty-3">Quantity, line <span data-n>3</span></label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib1-qty-3" value="8"></td><td class=""><label class="sr-only" for="ib1-price-3">Unit price, line <span data-n>3</span></label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib1-price-3" value="95"></td><td class=" num ib-amt" data-amount>$760.00</td><td class=" ib-rmcell"><button class="ib-rm" type="button" data-remove aria-label="Remove line 3" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr></tbody>
  </table>
  </div>
  <template data-line-template><tr data-line class=""><td class=""><label class="sr-only" data-label="desc">Description, line <span data-n>0</span></label><input class="ib-input" type="text" data-field="desc" value=""></td><td class=""><label class="sr-only" data-label="qty">Quantity, line <span data-n>0</span></label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" value="1"></td><td class=""><label class="sr-only" data-label="price">Unit price, line <span data-n>0</span></label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" value="0"></td><td class=" num ib-amt" data-amount>$0.00</td><td class=" ib-rmcell"><button class="ib-rm" type="button" data-remove aria-label="Remove line 0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr></template>
  <button class="ib-add" type="button" data-add hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add line</button>
  <div class="ib-bottom">
    <div class="ib-field"><label class="ib-label" for="ib1-notes">Notes for the client</label><textarea class="ib-input ib-textarea" id="ib1-notes" rows="4">50% of the total is due on approval of the designs, the rest on launch. Thank you.</textarea></div>
    <div class="ib-sumcol">
      <div class="ib-adj">
        <div class="ib-field"><label class="ib-label" for="ib1-discount">Discount %</label><input class="ib-input ib-num" id="ib1-discount" type="number" min="0" max="100" step="any" inputmode="decimal" value="5" data-discount></div>
        <div class="ib-field"><label class="ib-label" for="ib1-tax">Tax %</label><input class="ib-input ib-num" id="ib1-tax" type="number" min="0" max="100" step="any" inputmode="decimal" value="10" data-tax></div>
      </div>
      <dl class="ib-totals"><div class="ib-trow"><dt>Subtotal</dt><dd><output data-out="subtotal">$4,000.00</output></dd></div><div class="ib-trow"><dt>Discount (<span data-out-rate="discount">5</span>%)</dt><dd><output data-out="discount">−$200.00</output></dd></div><div class="ib-trow"><dt>Tax (<span data-out-rate="tax">10</span>%)</dt><dd><output data-out="tax">$380.00</output></dd></div><div class="ib-trow ib-trow--total"><dt>Total</dt><dd><output data-out="total">$4,180.00</output></dd></div></dl>
    </div>
  </div>
  <p class="sr-only" role="status" data-status></p>
</form>
</div>
<div><span class="lbl">Compact builder for a narrow panel</span>
<form class="ib ib--narrow" id="ib2" data-builder aria-labelledby="ib2-title" novalidate>
  <div class="ib-head"><div><h2 class="ib-title" id="ib2-title">Quick invoice</h2><p class="ib-sub">Tidewater Plumbing · job 7716</p></div></div>
  <div class="ib-field"><label class="ib-label" for="ib2-cur">Currency</label><select class="ib-input ib-select" id="ib2-cur" data-currency><option value="USD" data-locale="en-US">US dollar (USD)</option><option value="EUR" data-locale="de-DE">Euro (EUR)</option><option value="GBP" data-locale="en-GB" selected>Pound sterling (GBP)</option><option value="JPY" data-locale="ja-JP">Japanese yen (JPY)</option><option value="CAD" data-locale="en-CA">Canadian dollar (CAD)</option></select></div>
  <ol class="ib-clist" data-lines aria-label="Line items"><li data-line class="ib-cline"><fieldset class="ib-cfs"><legend class="ib-clegend">Line <span data-n>1</span></legend><div class="ib-cfull"><label class="ib-label" for="ib2-desc-1">Description</label><input class="ib-input" type="text" data-field="desc" id="ib2-desc-1" value="Call-out fee"></div><div><label class="ib-label" for="ib2-qty-1">Qty</label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib2-qty-1" value="1"></div><div><label class="ib-label" for="ib2-price-1">Price</label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib2-price-1" value="95"></div><p class="ib-camt"><span>Amount</span> <b data-amount>£95.00</b></p><button class="ib-rm ib-crm" type="button" data-remove aria-label="Remove line 1" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li><li data-line class="ib-cline"><fieldset class="ib-cfs"><legend class="ib-clegend">Line <span data-n>2</span></legend><div class="ib-cfull"><label class="ib-label" for="ib2-desc-2">Description</label><input class="ib-input" type="text" data-field="desc" id="ib2-desc-2" value="Labour, hours"></div><div><label class="ib-label" for="ib2-qty-2">Qty</label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib2-qty-2" value="2.5"></div><div><label class="ib-label" for="ib2-price-2">Price</label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib2-price-2" value="88"></div><p class="ib-camt"><span>Amount</span> <b data-amount>£220.00</b></p><button class="ib-rm ib-crm" type="button" data-remove aria-label="Remove line 2" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li><li data-line class="ib-cline"><fieldset class="ib-cfs"><legend class="ib-clegend">Line <span data-n>3</span></legend><div class="ib-cfull"><label class="ib-label" for="ib2-desc-3">Description</label><input class="ib-input" type="text" data-field="desc" id="ib2-desc-3" value="Thermostatic mixing valve"></div><div><label class="ib-label" for="ib2-qty-3">Qty</label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib2-qty-3" value="1"></div><div><label class="ib-label" for="ib2-price-3">Price</label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib2-price-3" value="64.5"></div><p class="ib-camt"><span>Amount</span> <b data-amount>£64.50</b></p><button class="ib-rm ib-crm" type="button" data-remove aria-label="Remove line 3" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li></ol>
  <template data-line-template><li data-line class="ib-cline"><fieldset class="ib-cfs"><legend class="ib-clegend">Line <span data-n>0</span></legend><div class="ib-cfull"><label class="ib-label" data-label="desc">Description</label><input class="ib-input" type="text" data-field="desc" value=""></div><div><label class="ib-label" data-label="qty">Qty</label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" value="1"></div><div><label class="ib-label" data-label="price">Price</label><input class="ib-input ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" value="0"></div><p class="ib-camt"><span>Amount</span> <b data-amount>£0.00</b></p><button class="ib-rm ib-crm" type="button" data-remove aria-label="Remove line 0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li></template>
  <button class="ib-add" type="button" data-add hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add line</button>
  <div class="ib-adj">
    <div class="ib-field"><label class="ib-label" for="ib2-tax">VAT %</label><input class="ib-input ib-num" id="ib2-tax" type="number" min="0" max="100" step="any" inputmode="decimal" value="20" data-tax></div>
  </div>
  <dl class="ib-totals"><div class="ib-trow"><dt>Subtotal</dt><dd><output data-out="subtotal">£379.50</output></dd></div><div class="ib-trow"><dt>VAT (<span data-out-rate="tax">20</span>%)</dt><dd><output data-out="tax">£75.90</output></dd></div><div class="ib-trow ib-trow--total"><dt>Total</dt><dd><output data-out="total">£455.40</output></dd></div></dl>
  <button class="ib-btn ib-btn--full" type="submit">Create invoice</button>
  <p class="sr-only" role="status" data-status></p>
</form>
</div>
CSS
/* Invoice builder. Each line's amount is qty × unit price, rounded to the currency's minor unit; discount and tax are
   percentages; Intl.NumberFormat formats every figure for the chosen currency and locale. Front end only: the
   form never submits. Line items are a real table (desktop) or a list of fieldsets (narrow panel), all labelled. */
.ib{--ib-accent:#0f766e;--ib-accent-dark:#115e59;--ib-ink:#13201f;--ib-muted:#4a5b59;--ib-line:#dbe5e3;--ib-field:#71817f;--ib-soft:#f1f6f5;--ib-ring:rgba(15,118,110,.28);
  box-sizing:border-box;background:#fff;color:var(--ib-ink);border-radius:18px;padding:28px;font-size:14px;line-height:1.45;
  box-shadow:0 1px 2px rgba(19,32,31,.06),0 24px 48px -30px rgba(19,32,31,.4)}
.ib *,.ib *::before,.ib *::after{box-sizing:border-box}
:where(.ib) :is(h2,p,dl,dd,fieldset,ol,table){margin:0}
.ib .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.ib :where(input,select,textarea,button){font:inherit;color:inherit}
.ib-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:22px}
.ib-title{font-size:1.35rem;font-weight:700;letter-spacing:-.01em}
.ib-sub{color:var(--ib-muted);font-size:.86rem;margin-top:2px}
.ib-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 18px;border:0;border-radius:10px;background:var(--ib-accent);color:#fff;font-weight:700;cursor:pointer;transition:background-color .15s}
.ib-btn:hover{background:var(--ib-accent-dark)}
.ib-btn--full{width:100%;margin-top:18px;padding:12px}
.ib-fs{border:0;padding:0;min-width:0;margin-bottom:22px}
.ib-legend{font-size:.74rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--ib-muted);margin-bottom:10px;padding:0}
.ib-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}
.ib-field--2{grid-column:span 2}
.ib-label{display:block;font-size:.8rem;font-weight:600;margin-bottom:5px}
.ib-input{display:block;width:100%;min-height:40px;padding:8px 10px;border:1px solid var(--ib-field);border-radius:9px;background:#fff;transition:border-color .15s,box-shadow .15s}
.ib-input:focus{outline:none;border-color:var(--ib-accent);box-shadow:0 0 0 3px var(--ib-ring)}
.ib-input:focus-visible{outline:2px solid var(--ib-accent);outline-offset:1px}
.ib-num{text-align:right;font-variant-numeric:tabular-nums}
.ib-select{appearance:none;padding-right:30px;background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2313201f' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 10px center}
.ib-textarea{min-height:96px;resize:vertical}
.ib-scroll{position:relative;overflow-x:auto;margin:0 -4px;padding:0 4px}
.ib-table{width:100%;min-width:560px;border-collapse:collapse;font-variant-numeric:tabular-nums}
.ib-caption{text-align:left;font-size:.74rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--ib-muted);padding-bottom:8px}
.ib-table thead th{padding:8px 6px;background:var(--ib-soft);font-size:.74rem;font-weight:600;color:var(--ib-muted);text-align:left;text-transform:uppercase;letter-spacing:.06em}
.ib-table thead th:first-child{border-radius:8px 0 0 8px}.ib-table thead th:last-child{border-radius:0 8px 8px 0}
.ib-table th:nth-child(2){width:84px}.ib-table th:nth-child(3){width:120px}.ib-table th:nth-child(4){width:120px}.ib-table th:nth-child(5){width:48px}
.ib-table td{padding:8px 6px;border-bottom:1px solid var(--ib-line);vertical-align:middle}
.ib-table .num{text-align:right}
.ib-amt{font-weight:600;white-space:nowrap}
.ib-rm{display:inline-grid;place-items:center;width:36px;height:36px;border:1px solid transparent;border-radius:9px;background:none;color:var(--ib-muted);cursor:pointer;transition:background-color .15s,color .15s}
.ib-rm:hover{background:#fdecea;color:#b42318}
.ib-rm[hidden],.ib-add[hidden]{display:none}
.ib-add{display:inline-flex;align-items:center;gap:6px;margin-top:12px;padding:9px 14px;border:1.5px dashed var(--ib-field);border-radius:10px;background:none;color:var(--ib-accent-dark);font-weight:700;cursor:pointer;transition:border-color .15s,background-color .15s}
.ib-add:hover{border-color:var(--ib-accent);background:var(--ib-soft)}
.ib :is(.ib-btn,.ib-rm,.ib-add):focus-visible{outline:2px solid var(--ib-accent);outline-offset:2px}
.ib-bottom{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:28px;margin-top:24px;align-items:start}
.ib-adj{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px}
.ib-totals{display:grid;gap:6px;padding:16px;border-radius:12px;background:var(--ib-soft);font-variant-numeric:tabular-nums}
.ib-trow{display:flex;justify-content:space-between;gap:12px}
.ib-trow dt{color:var(--ib-muted)}
.ib-trow dd{font-weight:600}
.ib-trow--total{margin-top:6px;padding-top:10px;border-top:1px solid #c3d3d0;align-items:baseline}
.ib-trow--total dt{color:var(--ib-ink);font-weight:700}
.ib-trow--total dd{font-size:1.45rem;font-weight:800;color:var(--ib-accent-dark)}
/* compact variant */
.ib--narrow{max-width:400px;padding:22px}
.ib--narrow>.ib-field{margin-bottom:14px}
.ib-clist{list-style:none;padding:0;display:grid;gap:10px}
.ib-cfs{position:relative;display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px 14px 14px;border:1px solid var(--ib-line);border-radius:12px;min-width:0}
.ib-clegend{float:left;font-size:.74rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ib-muted);padding:0}
.ib-clegend+*{clear:left}
.ib-cfull{grid-column:1/-1}
.ib-camt{grid-column:1/-1;display:flex;justify-content:space-between;font-variant-numeric:tabular-nums}
.ib-camt span{color:var(--ib-muted)}
.ib-crm{position:absolute;top:4px;right:6px;width:32px;height:32px}
.ib--narrow .ib-adj{grid-template-columns:1fr;margin-top:16px}
@media (max-width:720px){.ib-grid{grid-template-columns:1fr 1fr}.ib-bottom{grid-template-columns:1fr}}
@media (max-width:480px){.ib{padding:20px 16px}.ib-grid{grid-template-columns:1fr}.ib-field--2{grid-column:auto}.ib-head{flex-wrap:wrap}}
@media (prefers-reduced-motion:reduce){.ib-input,.ib-btn,.ib-rm,.ib-add{transition:none}}
JavaScript
// Invoice builder: add/remove lines, qty × price, discount %, tax %, totals in the chosen currency.
// Amounts are rounded per line in the currency's minor unit (cents, pence; none for yen), so they always add up.
// Front end only: nothing is saved or sent. Hooks are data attributes, so any stylesheet works.
document.querySelectorAll('[data-builder]').forEach(function (root) {
  var lines = root.querySelector('[data-lines]');
  var tpl = root.querySelector('template[data-line-template]');
  var addBtn = root.querySelector('[data-add]');
  var currency = root.querySelector('[data-currency]');
  var tax = root.querySelector('[data-tax]');
  var discount = root.querySelector('[data-discount]');
  var status = root.querySelector('[data-status]');
  if (!lines || !tpl || !currency) return;
  var uid = lines.querySelectorAll('[data-line]').length;

  function locale() { var o = currency.selectedOptions[0]; return (o && o.dataset.locale) || 'en-US'; }
  function fmt(minor, digits) {
    return new Intl.NumberFormat(locale(), { style: 'currency', currency: currency.value }).format(minor / Math.pow(10, digits));
  }
  function digits() { return new Intl.NumberFormat('en', { style: 'currency', currency: currency.value }).resolvedOptions().maximumFractionDigits; }
  function val(input, max) { var n = parseFloat(input && input.value); if (!isFinite(n) || n < 0) n = 0; return max ? Math.min(n, max) : n; }
  function out(name, text) { var el = root.querySelector('[data-out="' + name + '"]'); if (el) el.textContent = text; }

  function recalc() {
    var d = digits(), f = Math.pow(10, d), sub = 0;
    lines.querySelectorAll('[data-line]').forEach(function (line, i) {
      var amount = Math.round(val(line.querySelector('[data-field="qty"]')) * val(line.querySelector('[data-field="price"]')) * f);
      sub += amount;
      line.querySelector('[data-amount]').textContent = fmt(amount, d);
      line.querySelectorAll('[data-n]').forEach(function (n) { n.textContent = i + 1; });
      var rm = line.querySelector('[data-remove]'); if (rm) rm.setAttribute('aria-label', 'Remove line ' + (i + 1));
    });
    var off = Math.round(sub * val(discount, 100) / 100);
    var taxed = Math.round((sub - off) * val(tax, 100) / 100);
    out('subtotal', fmt(sub, d)); out('discount', off ? '−' + fmt(off, d) : fmt(0, d)); out('tax', fmt(taxed, d)); out('total', fmt(sub - off + taxed, d));
    root.querySelectorAll('[data-out-rate="discount"]').forEach(function (s) { s.textContent = val(discount, 100); });
    root.querySelectorAll('[data-out-rate="tax"]').forEach(function (s) { s.textContent = val(tax, 100); });
    return fmt(sub - off + taxed, d);
  }
  function say(text) { if (status) { status.textContent = ''; setTimeout(function () { status.textContent = text; }, 50); } }

  function addLine() {
    uid += 1;
    var frag = tpl.content.cloneNode(true), line = frag.querySelector('[data-line]');
    line.querySelectorAll('[data-field]').forEach(function (input) {
      var id = root.id + '-' + input.dataset.field + '-' + uid; input.id = id;
      var label = line.querySelector('[data-label="' + input.dataset.field + '"]'); if (label) label.htmlFor = id;
    });
    lines.appendChild(frag);
    var total = recalc();
    line.querySelector('[data-field="desc"]').focus();
    say('Line ' + lines.querySelectorAll('[data-line]').length + ' added. Total ' + total + '.');
  }

  root.addEventListener('click', function (e) {
    var rm = e.target.closest('[data-remove]');
    if (!rm) return;
    var line = rm.closest('[data-line]');
    var next = line.nextElementSibling || line.previousElementSibling;
    line.remove();
    var total = recalc();
    (next && next.matches('[data-line]') ? next.querySelector('[data-remove]') : addBtn).focus();
    say('Line removed. Total ' + total + '.');
  });
  root.addEventListener('input', recalc);
  root.addEventListener('change', function (e) {
    if (e.target.matches('[data-field="qty"],[data-field="price"],[data-tax],[data-discount],[data-currency]')) say('Total ' + recalc() + '.');
  });
  root.addEventListener('submit', function (e) { e.preventDefault(); say('Demo only: this invoice was not saved or sent. Total ' + recalc() + '.'); });
  if (addBtn) { addBtn.hidden = false; addBtn.addEventListener('click', addLine); }
  root.querySelectorAll('[data-remove]').forEach(function (b) { b.hidden = false; });
  recalc();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Invoice builder with editable line items</span>
<form class="ib card" id="ib1" data-builder aria-labelledby="ib1-title" novalidate>
  <div class="d-flex justify-content-between align-items-start gap-3 mb-4 flex-wrap">
    <div><h2 class="ib-title" id="ib1-title">New invoice</h2><p class="ib-sub">Mossgrove Web Studio · draft, not sent</p></div>
    <button class="btn btn-primary" type="submit">Save draft</button>
  </div>
  <fieldset class="mb-4"><legend class="ib-legend">Details</legend>
    <div class="row g-3">
      <div class="col-sm-6 col-md-3"><label class="form-label" for="ib1-no">Invoice number</label><input class="form-control" id="ib1-no" type="text" value="MW-2026-031"></div>
      <div class="col-sm-6 col-md-3"><label class="form-label" for="ib1-issued">Issue date</label><input class="form-control" id="ib1-issued" type="date" value="2026-10-19"></div>
      <div class="col-sm-6 col-md-3"><label class="form-label" for="ib1-due">Due date</label><input class="form-control" id="ib1-due" type="date" value="2026-11-18"></div>
      <div class="col-sm-6 col-md-3"><label class="form-label" for="ib1-cur">Currency</label><select class="form-select" id="ib1-cur" data-currency><option value="USD" data-locale="en-US" selected>US dollar (USD)</option><option value="EUR" data-locale="de-DE">Euro (EUR)</option><option value="GBP" data-locale="en-GB">Pound sterling (GBP)</option><option value="JPY" data-locale="ja-JP">Japanese yen (JPY)</option><option value="CAD" data-locale="en-CA">Canadian dollar (CAD)</option></select></div>
      <div class="col-md-6"><label class="form-label" for="ib1-client">Bill to</label><input class="form-control" id="ib1-client" type="text" value="Kettle & Crane Tea Rooms" autocomplete="organization"></div>
      <div class="col-md-6"><label class="form-label" for="ib1-email">Client email</label><input class="form-control" id="ib1-email" type="email" value="[email protected]" autocomplete="email"></div>
    </div>
  </fieldset>
  <div class="table-responsive position-relative">
  <table class="table align-middle caption-top">
    <caption class="ib-caption">Line items</caption>
    <thead><tr class=""><th scope="col" class=" ">Description</th><th scope="col" class=" text-end">Qty</th><th scope="col" class=" text-end">Unit price</th><th scope="col" class=" text-end">Amount</th><th scope="col" class=" "><span class="visually-hidden">Remove</span></th></tr></thead>
    <tbody data-lines><tr data-line class=""><td class=""><label class="visually-hidden" for="ib1-desc-1">Description, line <span data-n>1</span></label><input class="form-control" type="text" data-field="desc" id="ib1-desc-1" value="Discovery workshop"></td><td class=""><label class="visually-hidden" for="ib1-qty-1">Quantity, line <span data-n>1</span></label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib1-qty-1" value="1"></td><td class=""><label class="visually-hidden" for="ib1-price-1">Unit price, line <span data-n>1</span></label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib1-price-1" value="1200"></td><td class=" text-end ib-amt" data-amount>$1,200.00</td><td class=" text-end"><button class="btn ib-rm" type="button" data-remove aria-label="Remove line 1" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr><tr data-line class=""><td class=""><label class="visually-hidden" for="ib1-desc-2">Description, line <span data-n>2</span></label><input class="form-control" type="text" data-field="desc" id="ib1-desc-2" value="Page templates"></td><td class=""><label class="visually-hidden" for="ib1-qty-2">Quantity, line <span data-n>2</span></label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib1-qty-2" value="6"></td><td class=""><label class="visually-hidden" for="ib1-price-2">Unit price, line <span data-n>2</span></label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib1-price-2" value="340"></td><td class=" text-end ib-amt" data-amount>$2,040.00</td><td class=" text-end"><button class="btn ib-rm" type="button" data-remove aria-label="Remove line 2" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr><tr data-line class=""><td class=""><label class="visually-hidden" for="ib1-desc-3">Description, line <span data-n>3</span></label><input class="form-control" type="text" data-field="desc" id="ib1-desc-3" value="Copywriting, per page"></td><td class=""><label class="visually-hidden" for="ib1-qty-3">Quantity, line <span data-n>3</span></label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib1-qty-3" value="8"></td><td class=""><label class="visually-hidden" for="ib1-price-3">Unit price, line <span data-n>3</span></label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib1-price-3" value="95"></td><td class=" text-end ib-amt" data-amount>$760.00</td><td class=" text-end"><button class="btn ib-rm" type="button" data-remove aria-label="Remove line 3" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr></tbody>
  </table>
  </div>
  <template data-line-template><tr data-line class=""><td class=""><label class="visually-hidden" data-label="desc">Description, line <span data-n>0</span></label><input class="form-control" type="text" data-field="desc" value=""></td><td class=""><label class="visually-hidden" data-label="qty">Quantity, line <span data-n>0</span></label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" value="1"></td><td class=""><label class="visually-hidden" data-label="price">Unit price, line <span data-n>0</span></label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" value="0"></td><td class=" text-end ib-amt" data-amount>$0.00</td><td class=" text-end"><button class="btn ib-rm" type="button" data-remove aria-label="Remove line 0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr></template>
  <button class="btn ib-add align-self-start mt-2" type="button" data-add hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add line</button>
  <div class="row g-4 mt-1 align-items-start">
    <div class="col-md"><label class="form-label" for="ib1-notes">Notes for the client</label><textarea class="form-control" id="ib1-notes" rows="4">50% of the total is due on approval of the designs, the rest on launch. Thank you.</textarea></div>
    <div class="col-md-5 col-lg-4">
      <div class="row g-2 mb-3 ib-adj">
        <div class="col-6"><label class="form-label" for="ib1-discount">Discount %</label><input class="form-control text-end ib-num" id="ib1-discount" type="number" min="0" max="100" step="any" inputmode="decimal" value="5" data-discount></div>
        <div class="col-6"><label class="form-label" for="ib1-tax">Tax %</label><input class="form-control text-end ib-num" id="ib1-tax" type="number" min="0" max="100" step="any" inputmode="decimal" value="10" data-tax></div>
      </div>
      <dl class="ib-totals d-grid gap-1 p-3"><div class="d-flex justify-content-between gap-3"><dt>Subtotal</dt><dd><output data-out="subtotal">$4,000.00</output></dd></div><div class="d-flex justify-content-between gap-3"><dt>Discount (<span data-out-rate="discount">5</span>%)</dt><dd><output data-out="discount">−$200.00</output></dd></div><div class="d-flex justify-content-between gap-3"><dt>Tax (<span data-out-rate="tax">10</span>%)</dt><dd><output data-out="tax">$380.00</output></dd></div><div class="ib-total d-flex justify-content-between align-items-baseline gap-3 mt-1 pt-2"><dt>Total</dt><dd><output data-out="total">$4,180.00</output></dd></div></dl>
    </div>
  </div>
  <p class="visually-hidden" role="status" data-status></p>
</form>
</div>
<div><span class="lbl">Compact builder for a narrow panel</span>
<form class="ib card ib--narrow" id="ib2" data-builder aria-labelledby="ib2-title" novalidate>
  <div class="d-flex justify-content-between align-items-start gap-3 mb-4 flex-wrap"><div><h2 class="ib-title" id="ib2-title">Quick invoice</h2><p class="ib-sub">Tidewater Plumbing · job 7716</p></div></div>
  <div class="mb-3"><label class="form-label" for="ib2-cur">Currency</label><select class="form-select" id="ib2-cur" data-currency><option value="USD" data-locale="en-US">US dollar (USD)</option><option value="EUR" data-locale="de-DE">Euro (EUR)</option><option value="GBP" data-locale="en-GB" selected>Pound sterling (GBP)</option><option value="JPY" data-locale="ja-JP">Japanese yen (JPY)</option><option value="CAD" data-locale="en-CA">Canadian dollar (CAD)</option></select></div>
  <ol class="list-unstyled d-grid gap-2 mb-0" data-lines aria-label="Line items"><li data-line class=""><fieldset class="ib-cfs row g-2 position-relative mx-0"><legend class="ib-legend float-start col-12 mb-0">Line <span data-n>1</span></legend><div class="col-12"><label class="form-label" for="ib2-desc-1">Description</label><input class="form-control" type="text" data-field="desc" id="ib2-desc-1" value="Call-out fee"></div><div><label class="form-label" for="ib2-qty-1">Qty</label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib2-qty-1" value="1"></div><div><label class="form-label" for="ib2-price-1">Price</label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib2-price-1" value="95"></div><p class="col-12 d-flex justify-content-between ib-num mb-0"><span>Amount</span> <b data-amount>£95.00</b></p><button class="btn ib-rm position-absolute top-0 end-0 mt-1 me-1" type="button" data-remove aria-label="Remove line 1" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li><li data-line class=""><fieldset class="ib-cfs row g-2 position-relative mx-0"><legend class="ib-legend float-start col-12 mb-0">Line <span data-n>2</span></legend><div class="col-12"><label class="form-label" for="ib2-desc-2">Description</label><input class="form-control" type="text" data-field="desc" id="ib2-desc-2" value="Labour, hours"></div><div><label class="form-label" for="ib2-qty-2">Qty</label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib2-qty-2" value="2.5"></div><div><label class="form-label" for="ib2-price-2">Price</label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib2-price-2" value="88"></div><p class="col-12 d-flex justify-content-between ib-num mb-0"><span>Amount</span> <b data-amount>£220.00</b></p><button class="btn ib-rm position-absolute top-0 end-0 mt-1 me-1" type="button" data-remove aria-label="Remove line 2" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li><li data-line class=""><fieldset class="ib-cfs row g-2 position-relative mx-0"><legend class="ib-legend float-start col-12 mb-0">Line <span data-n>3</span></legend><div class="col-12"><label class="form-label" for="ib2-desc-3">Description</label><input class="form-control" type="text" data-field="desc" id="ib2-desc-3" value="Thermostatic mixing valve"></div><div><label class="form-label" for="ib2-qty-3">Qty</label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib2-qty-3" value="1"></div><div><label class="form-label" for="ib2-price-3">Price</label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib2-price-3" value="64.5"></div><p class="col-12 d-flex justify-content-between ib-num mb-0"><span>Amount</span> <b data-amount>£64.50</b></p><button class="btn ib-rm position-absolute top-0 end-0 mt-1 me-1" type="button" data-remove aria-label="Remove line 3" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li></ol>
  <template data-line-template><li data-line class=""><fieldset class="ib-cfs row g-2 position-relative mx-0"><legend class="ib-legend float-start col-12 mb-0">Line <span data-n>0</span></legend><div class="col-12"><label class="form-label" data-label="desc">Description</label><input class="form-control" type="text" data-field="desc" value=""></div><div><label class="form-label" data-label="qty">Qty</label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="qty" value="1"></div><div><label class="form-label" data-label="price">Price</label><input class="form-control text-end ib-num" type="number" min="0" step="any" inputmode="decimal" data-field="price" value="0"></div><p class="col-12 d-flex justify-content-between ib-num mb-0"><span>Amount</span> <b data-amount>£0.00</b></p><button class="btn ib-rm position-absolute top-0 end-0 mt-1 me-1" type="button" data-remove aria-label="Remove line 0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li></template>
  <button class="btn ib-add align-self-start mt-2" type="button" data-add hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add line</button>
  <div class="row g-2 mb-3 ib-adj">
    <div class="col-6"><label class="form-label" for="ib2-tax">VAT %</label><input class="form-control text-end ib-num" id="ib2-tax" type="number" min="0" max="100" step="any" inputmode="decimal" value="20" data-tax></div>
  </div>
  <dl class="ib-totals d-grid gap-1 p-3"><div class="d-flex justify-content-between gap-3"><dt>Subtotal</dt><dd><output data-out="subtotal">£379.50</output></dd></div><div class="d-flex justify-content-between gap-3"><dt>VAT (<span data-out-rate="tax">20</span>%)</dt><dd><output data-out="tax">£75.90</output></dd></div><div class="ib-total d-flex justify-content-between align-items-baseline gap-3 mt-1 pt-2"><dt>Total</dt><dd><output data-out="total">£455.40</output></dd></div></dl>
  <button class="btn btn-primary w-100 mt-3 py-2" type="submit">Create invoice</button>
  <p class="visually-hidden" role="status" data-status></p>
</form>
</div>
CSS
/* Bootstrap .card, .row/.col, .form-control, .form-select, .table in .table-responsive and .btn, retuned through
   Bootstrap's CSS variables. The script recalculates every line and total and formats them with Intl.NumberFormat. */
.ib{--ib-accent:#0f766e;--ib-accent-dark:#115e59;--ib-ink:#13201f;--ib-muted:#4a5b59;--ib-line:#dbe5e3;--ib-field:#71817f;--ib-soft:#f1f6f5;--ib-ring:rgba(15,118,110,.28);
  --bs-card-border-width:0;--bs-card-border-radius:18px;--bs-body-color:var(--ib-ink);--bs-secondary-color:var(--ib-muted);--bs-border-color:var(--ib-field);--bs-border-radius:9px;
  color:var(--ib-ink);padding:28px;font-size:14px;line-height:1.45;box-shadow:0 1px 2px rgba(19,32,31,.06),0 24px 48px -30px rgba(19,32,31,.4)}
.ib :is(h2,p,dl,dd,fieldset){margin:0}
.ib-title{font-size:1.35rem;font-weight:700;letter-spacing:-.01em}
.ib-sub{color:var(--ib-muted);font-size:.86rem;margin-top:2px!important}
.ib-legend{font-size:.74rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--ib-muted);margin-bottom:10px}
.ib .form-label{font-size:.8rem;font-weight:600;margin-bottom:5px}
.ib :is(.form-control,.form-select){font-size:inherit;min-height:40px;padding:8px 10px}
.ib .form-select{padding-right:30px;background-position:right 10px center}
.ib :is(.form-control,.form-select):focus{border-color:var(--ib-accent);box-shadow:0 0 0 3px var(--ib-ring);outline:2px solid var(--ib-accent);outline-offset:1px}
.ib textarea.form-control{min-height:96px}
.ib-num{font-variant-numeric:tabular-nums}
.ib .btn-primary{--bs-btn-bg:var(--ib-accent);--bs-btn-border-color:var(--ib-accent);--bs-btn-hover-bg:var(--ib-accent-dark);--bs-btn-hover-border-color:var(--ib-accent-dark);--bs-btn-active-bg:var(--ib-accent-dark);
  --bs-btn-font-weight:700;--bs-btn-padding-x:18px;--bs-btn-padding-y:10px;--bs-btn-border-radius:10px;--bs-btn-font-size:inherit}
.ib .btn:focus-visible{outline:2px solid var(--ib-accent);outline-offset:2px;box-shadow:none}
.ib .table{--bs-table-bg:transparent;--bs-table-color:var(--ib-ink);--bs-table-border-color:var(--ib-line);min-width:560px;font-variant-numeric:tabular-nums;margin:0}
.ib-caption{color:var(--ib-muted)!important;font-size:.74rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;padding:0 0 8px!important}
.ib .table thead th{padding:8px 6px;background:var(--ib-soft);border:0;font-size:.74rem;font-weight:600;color:var(--ib-muted);text-transform:uppercase;letter-spacing:.06em}
.ib .table thead th:first-child{border-radius:8px 0 0 8px}.ib .table thead th:last-child{border-radius:0 8px 8px 0}
.ib .table th:nth-child(2){width:84px}.ib .table th:nth-child(3){width:120px}.ib .table th:nth-child(4){width:120px}.ib .table th:nth-child(5){width:48px}
.ib .table td{padding:8px 6px}
.ib-amt{font-weight:600;white-space:nowrap}
.ib-rm{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-color:var(--ib-muted);--bs-btn-hover-bg:#fdecea;--bs-btn-hover-color:#b42318;--bs-btn-border-radius:9px;width:36px;height:36px;display:inline-grid;place-items:center}
.ib-add{--bs-btn-color:var(--ib-accent-dark);--bs-btn-border-color:var(--ib-field);--bs-btn-hover-bg:var(--ib-soft);--bs-btn-hover-border-color:var(--ib-accent);--bs-btn-hover-color:var(--ib-accent-dark);
  --bs-btn-font-weight:700;--bs-btn-border-radius:10px;--bs-btn-border-width:1.5px;--bs-btn-padding-x:14px;--bs-btn-font-size:inherit;border-style:dashed;display:inline-flex;align-items:center;gap:8px}
.ib-totals{border-radius:12px;background:var(--ib-soft);font-variant-numeric:tabular-nums}
.ib-totals dt{font-weight:400;color:var(--ib-muted)}
.ib-totals dd{font-weight:600}
.ib-total{border-top:1px solid #c3d3d0}
.ib-total dt{color:var(--ib-ink)!important;font-weight:700!important}
.ib-total dd{font-size:1.45rem;font-weight:800;color:var(--ib-accent-dark)}
.ib--narrow{max-width:400px;padding:22px}
.ib--narrow .ib-adj>*{width:100%}
.ib--narrow .ib-adj{margin-top:6px}
.ib-cfs{padding:12px 6px 14px;border:1px solid var(--ib-line);border-radius:12px}
.ib-cfs>legend{font-size:.74rem;padding-left:calc(var(--bs-gutter-x)*.5)}
.ib-cfs>legend+*{clear:left}
.ib-cfs>div:not(.col-12){width:50%}
.ib-crm{width:32px!important;height:32px}
@media (max-width:480px){.ib{padding:20px 16px}}
JavaScript
// Invoice builder: add/remove lines, qty × price, discount %, tax %, totals in the chosen currency.
// Amounts are rounded per line in the currency's minor unit (cents, pence; none for yen), so they always add up.
// Front end only: nothing is saved or sent. Hooks are data attributes, so any stylesheet works.
document.querySelectorAll('[data-builder]').forEach(function (root) {
  var lines = root.querySelector('[data-lines]');
  var tpl = root.querySelector('template[data-line-template]');
  var addBtn = root.querySelector('[data-add]');
  var currency = root.querySelector('[data-currency]');
  var tax = root.querySelector('[data-tax]');
  var discount = root.querySelector('[data-discount]');
  var status = root.querySelector('[data-status]');
  if (!lines || !tpl || !currency) return;
  var uid = lines.querySelectorAll('[data-line]').length;

  function locale() { var o = currency.selectedOptions[0]; return (o && o.dataset.locale) || 'en-US'; }
  function fmt(minor, digits) {
    return new Intl.NumberFormat(locale(), { style: 'currency', currency: currency.value }).format(minor / Math.pow(10, digits));
  }
  function digits() { return new Intl.NumberFormat('en', { style: 'currency', currency: currency.value }).resolvedOptions().maximumFractionDigits; }
  function val(input, max) { var n = parseFloat(input && input.value); if (!isFinite(n) || n < 0) n = 0; return max ? Math.min(n, max) : n; }
  function out(name, text) { var el = root.querySelector('[data-out="' + name + '"]'); if (el) el.textContent = text; }

  function recalc() {
    var d = digits(), f = Math.pow(10, d), sub = 0;
    lines.querySelectorAll('[data-line]').forEach(function (line, i) {
      var amount = Math.round(val(line.querySelector('[data-field="qty"]')) * val(line.querySelector('[data-field="price"]')) * f);
      sub += amount;
      line.querySelector('[data-amount]').textContent = fmt(amount, d);
      line.querySelectorAll('[data-n]').forEach(function (n) { n.textContent = i + 1; });
      var rm = line.querySelector('[data-remove]'); if (rm) rm.setAttribute('aria-label', 'Remove line ' + (i + 1));
    });
    var off = Math.round(sub * val(discount, 100) / 100);
    var taxed = Math.round((sub - off) * val(tax, 100) / 100);
    out('subtotal', fmt(sub, d)); out('discount', off ? '−' + fmt(off, d) : fmt(0, d)); out('tax', fmt(taxed, d)); out('total', fmt(sub - off + taxed, d));
    root.querySelectorAll('[data-out-rate="discount"]').forEach(function (s) { s.textContent = val(discount, 100); });
    root.querySelectorAll('[data-out-rate="tax"]').forEach(function (s) { s.textContent = val(tax, 100); });
    return fmt(sub - off + taxed, d);
  }
  function say(text) { if (status) { status.textContent = ''; setTimeout(function () { status.textContent = text; }, 50); } }

  function addLine() {
    uid += 1;
    var frag = tpl.content.cloneNode(true), line = frag.querySelector('[data-line]');
    line.querySelectorAll('[data-field]').forEach(function (input) {
      var id = root.id + '-' + input.dataset.field + '-' + uid; input.id = id;
      var label = line.querySelector('[data-label="' + input.dataset.field + '"]'); if (label) label.htmlFor = id;
    });
    lines.appendChild(frag);
    var total = recalc();
    line.querySelector('[data-field="desc"]').focus();
    say('Line ' + lines.querySelectorAll('[data-line]').length + ' added. Total ' + total + '.');
  }

  root.addEventListener('click', function (e) {
    var rm = e.target.closest('[data-remove]');
    if (!rm) return;
    var line = rm.closest('[data-line]');
    var next = line.nextElementSibling || line.previousElementSibling;
    line.remove();
    var total = recalc();
    (next && next.matches('[data-line]') ? next.querySelector('[data-remove]') : addBtn).focus();
    say('Line removed. Total ' + total + '.');
  });
  root.addEventListener('input', recalc);
  root.addEventListener('change', function (e) {
    if (e.target.matches('[data-field="qty"],[data-field="price"],[data-tax],[data-discount],[data-currency]')) say('Total ' + recalc() + '.');
  });
  root.addEventListener('submit', function (e) { e.preventDefault(); say('Demo only: this invoice was not saved or sent. Total ' + recalc() + '.'); });
  if (addBtn) { addBtn.hidden = false; addBtn.addEventListener('click', addLine); }
  root.querySelectorAll('[data-remove]').forEach(function (b) { b.hidden = false; });
  recalc();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4a5b59] uppercase">Invoice builder with editable line items</span>
<form class="rounded-[18px] bg-white p-7 text-[14px] leading-[1.45] text-ib-ink shadow-[0_1px_2px_rgba(19,32,31,.06),0_24px_48px_-30px_rgba(19,32,31,.4)] max-[480px]:px-4 max-[480px]:py-5" id="ib1" data-builder aria-labelledby="ib1-title" novalidate>
  <div class="mb-[22px] flex flex-wrap items-start justify-between gap-4">
    <div><h2 class="text-[1.35rem] font-bold tracking-[-.01em]" id="ib1-title">New invoice</h2><p class="mt-0.5 text-[.86rem] text-ib-muted">Mossgrove Web Studio · draft, not sent</p></div>
    <button class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-[10px] bg-ib-accent px-[18px] py-2.5 font-bold text-white transition-colors duration-150 hover:bg-ib-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent" type="submit">Save draft</button>
  </div>
  <fieldset class="mb-[22px] min-w-0"><legend class="mb-2.5 text-[.74rem] font-bold tracking-[.1em] text-ib-muted uppercase">Details</legend>
    <div class="grid grid-cols-4 gap-3.5 max-[720px]:grid-cols-2 max-[480px]:grid-cols-1">
      <div class=""><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib1-no">Invoice number</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" id="ib1-no" type="text" value="MW-2026-031"></div>
      <div class=""><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib1-issued">Issue date</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" id="ib1-issued" type="date" value="2026-10-19"></div>
      <div class=""><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib1-due">Due date</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" id="ib1-due" type="date" value="2026-11-18"></div>
      <div class=""><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib1-cur">Currency</label><select class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none appearance-none pr-[30px] bg-(image:--ib-chevron) bg-no-repeat bg-[position:right_10px_center]" id="ib1-cur" data-currency><option value="USD" data-locale="en-US" selected>US dollar (USD)</option><option value="EUR" data-locale="de-DE">Euro (EUR)</option><option value="GBP" data-locale="en-GB">Pound sterling (GBP)</option><option value="JPY" data-locale="ja-JP">Japanese yen (JPY)</option><option value="CAD" data-locale="en-CA">Canadian dollar (CAD)</option></select></div>
      <div class="col-span-2 max-[480px]:col-span-1"><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib1-client">Bill to</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" id="ib1-client" type="text" value="Kettle & Crane Tea Rooms" autocomplete="organization"></div>
      <div class="col-span-2 max-[480px]:col-span-1"><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib1-email">Client email</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" id="ib1-email" type="email" value="[email protected]" autocomplete="email"></div>
    </div>
  </fieldset>
  <div class="relative -mx-1 overflow-x-auto px-1">
  <table class="w-full min-w-[560px] border-collapse tabular-nums">
    <caption class="pb-2 text-left text-[.74rem] font-bold tracking-[.1em] text-ib-muted uppercase">Line items</caption>
    <thead><tr class="[&>th]:bg-ib-soft [&>th]:px-1.5 [&>th]:py-2 [&>th]:text-[.74rem] [&>th]:font-semibold [&>th]:tracking-[.06em] [&>th]:text-ib-muted [&>th]:uppercase [&>th:first-child]:rounded-l-lg [&>th:last-child]:rounded-r-lg [&>th:nth-child(2)]:w-[84px] [&>th:nth-child(3)]:w-[120px] [&>th:nth-child(4)]:w-[120px] [&>th:nth-child(5)]:w-12"><th scope="col" class=" text-left">Description</th><th scope="col" class=" text-right">Qty</th><th scope="col" class=" text-right">Unit price</th><th scope="col" class=" text-right">Amount</th><th scope="col" class=" "><span class="sr-only">Remove</span></th></tr></thead>
    <tbody data-lines><tr data-line class="[&>td]:border-b [&>td]:border-ib-line [&>td]:px-1.5 [&>td]:py-2 [&>td]:align-middle"><td class=""><label class="sr-only" for="ib1-desc-1">Description, line <span data-n>1</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" type="text" data-field="desc" id="ib1-desc-1" value="Discovery workshop"></td><td class=""><label class="sr-only" for="ib1-qty-1">Quantity, line <span data-n>1</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib1-qty-1" value="1"></td><td class=""><label class="sr-only" for="ib1-price-1">Unit price, line <span data-n>1</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib1-price-1" value="1200"></td><td class=" text-right font-semibold whitespace-nowrap" data-amount>$1,200.00</td><td class=" text-right"><button class="inline-grid size-9 cursor-pointer place-items-center rounded-[9px] text-ib-muted transition-colors duration-150 hover:bg-[#fdecea] hover:text-[#b42318] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent" type="button" data-remove aria-label="Remove line 1" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr><tr data-line class="[&>td]:border-b [&>td]:border-ib-line [&>td]:px-1.5 [&>td]:py-2 [&>td]:align-middle"><td class=""><label class="sr-only" for="ib1-desc-2">Description, line <span data-n>2</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" type="text" data-field="desc" id="ib1-desc-2" value="Page templates"></td><td class=""><label class="sr-only" for="ib1-qty-2">Quantity, line <span data-n>2</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib1-qty-2" value="6"></td><td class=""><label class="sr-only" for="ib1-price-2">Unit price, line <span data-n>2</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib1-price-2" value="340"></td><td class=" text-right font-semibold whitespace-nowrap" data-amount>$2,040.00</td><td class=" text-right"><button class="inline-grid size-9 cursor-pointer place-items-center rounded-[9px] text-ib-muted transition-colors duration-150 hover:bg-[#fdecea] hover:text-[#b42318] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent" type="button" data-remove aria-label="Remove line 2" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr><tr data-line class="[&>td]:border-b [&>td]:border-ib-line [&>td]:px-1.5 [&>td]:py-2 [&>td]:align-middle"><td class=""><label class="sr-only" for="ib1-desc-3">Description, line <span data-n>3</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" type="text" data-field="desc" id="ib1-desc-3" value="Copywriting, per page"></td><td class=""><label class="sr-only" for="ib1-qty-3">Quantity, line <span data-n>3</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib1-qty-3" value="8"></td><td class=""><label class="sr-only" for="ib1-price-3">Unit price, line <span data-n>3</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib1-price-3" value="95"></td><td class=" text-right font-semibold whitespace-nowrap" data-amount>$760.00</td><td class=" text-right"><button class="inline-grid size-9 cursor-pointer place-items-center rounded-[9px] text-ib-muted transition-colors duration-150 hover:bg-[#fdecea] hover:text-[#b42318] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent" type="button" data-remove aria-label="Remove line 3" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr></tbody>
  </table>
  </div>
  <template data-line-template><tr data-line class="[&>td]:border-b [&>td]:border-ib-line [&>td]:px-1.5 [&>td]:py-2 [&>td]:align-middle"><td class=""><label class="sr-only" data-label="desc">Description, line <span data-n>0</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" type="text" data-field="desc" value=""></td><td class=""><label class="sr-only" data-label="qty">Quantity, line <span data-n>0</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="qty" value="1"></td><td class=""><label class="sr-only" data-label="price">Unit price, line <span data-n>0</span></label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="price" value="0"></td><td class=" text-right font-semibold whitespace-nowrap" data-amount>$0.00</td><td class=" text-right"><button class="inline-grid size-9 cursor-pointer place-items-center rounded-[9px] text-ib-muted transition-colors duration-150 hover:bg-[#fdecea] hover:text-[#b42318] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent" type="button" data-remove aria-label="Remove line 0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></td></tr></template>
  <button class="mt-3 inline-flex cursor-pointer items-center gap-1.5 rounded-[10px] border-[1.5px] border-dashed border-ib-field px-3.5 py-[9px] font-bold text-ib-accent-dark transition-colors duration-150 hover:border-ib-accent hover:bg-ib-soft motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent" type="button" data-add hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add line</button>
  <div class="mt-6 grid grid-cols-[minmax(0,1fr)_300px] items-start gap-7 max-[720px]:grid-cols-1">
    <div class=""><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib1-notes">Notes for the client</label><textarea class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none min-h-24 resize-y" id="ib1-notes" rows="4">50% of the total is due on approval of the designs, the rest on launch. Thank you.</textarea></div>
    <div class="">
      <div class="mb-3.5 grid grid-cols-2 gap-3 [form:has(ol)_&]:mt-4 [form:has(ol)_&]:grid-cols-1">
        <div class=""><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib1-discount">Discount %</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" id="ib1-discount" type="number" min="0" max="100" step="any" inputmode="decimal" value="5" data-discount></div>
        <div class=""><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib1-tax">Tax %</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" id="ib1-tax" type="number" min="0" max="100" step="any" inputmode="decimal" value="10" data-tax></div>
      </div>
      <dl class="grid gap-1.5 rounded-xl bg-ib-soft p-4 tabular-nums"><div class="flex justify-between gap-3 [&>dt]:text-ib-muted [&>dd]:font-semibold"><dt>Subtotal</dt><dd><output data-out="subtotal">$4,000.00</output></dd></div><div class="flex justify-between gap-3 [&>dt]:text-ib-muted [&>dd]:font-semibold"><dt>Discount (<span data-out-rate="discount">5</span>%)</dt><dd><output data-out="discount">−$200.00</output></dd></div><div class="flex justify-between gap-3 [&>dt]:text-ib-muted [&>dd]:font-semibold"><dt>Tax (<span data-out-rate="tax">10</span>%)</dt><dd><output data-out="tax">$380.00</output></dd></div><div class="mt-1.5 flex items-baseline justify-between gap-3 border-t border-[#c3d3d0] pt-2.5 [&>dt]:font-bold [&>dd]:text-[1.45rem] [&>dd]:font-extrabold [&>dd]:text-ib-accent-dark"><dt>Total</dt><dd><output data-out="total">$4,180.00</output></dd></div></dl>
    </div>
  </div>
  <p class="sr-only" role="status" data-status></p>
</form>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4a5b59] uppercase">Compact builder for a narrow panel</span>
<form class="rounded-[18px] bg-white p-7 text-[14px] leading-[1.45] text-ib-ink shadow-[0_1px_2px_rgba(19,32,31,.06),0_24px_48px_-30px_rgba(19,32,31,.4)] max-[480px]:px-4 max-[480px]:py-5 max-w-[400px] p-[22px]! [&>div:has(>select)]:mb-3.5" id="ib2" data-builder aria-labelledby="ib2-title" novalidate>
  <div class="mb-[22px] flex flex-wrap items-start justify-between gap-4"><div><h2 class="text-[1.35rem] font-bold tracking-[-.01em]" id="ib2-title">Quick invoice</h2><p class="mt-0.5 text-[.86rem] text-ib-muted">Tidewater Plumbing · job 7716</p></div></div>
  <div class=""><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-cur">Currency</label><select class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none appearance-none pr-[30px] bg-(image:--ib-chevron) bg-no-repeat bg-[position:right_10px_center]" id="ib2-cur" data-currency><option value="USD" data-locale="en-US">US dollar (USD)</option><option value="EUR" data-locale="de-DE">Euro (EUR)</option><option value="GBP" data-locale="en-GB" selected>Pound sterling (GBP)</option><option value="JPY" data-locale="ja-JP">Japanese yen (JPY)</option><option value="CAD" data-locale="en-CA">Canadian dollar (CAD)</option></select></div>
  <ol class="grid gap-2.5" data-lines aria-label="Line items"><li data-line class=""><fieldset class="relative grid min-w-0 grid-cols-2 gap-2.5 rounded-xl border border-ib-line px-3.5 pt-3 pb-3.5"><legend class="float-left text-[.74rem] font-bold tracking-[.08em] text-ib-muted uppercase [&+*]:clear-left">Line <span data-n>1</span></legend><div class="col-span-2"><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-desc-1">Description</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" type="text" data-field="desc" id="ib2-desc-1" value="Call-out fee"></div><div><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-qty-1">Qty</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib2-qty-1" value="1"></div><div><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-price-1">Price</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib2-price-1" value="95"></div><p class="col-span-2 flex justify-between tabular-nums [&>span]:text-ib-muted"><span>Amount</span> <b data-amount>£95.00</b></p><button class="inline-grid size-9 cursor-pointer place-items-center rounded-[9px] text-ib-muted transition-colors duration-150 hover:bg-[#fdecea] hover:text-[#b42318] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent absolute! top-1 right-1.5 size-8!" type="button" data-remove aria-label="Remove line 1" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li><li data-line class=""><fieldset class="relative grid min-w-0 grid-cols-2 gap-2.5 rounded-xl border border-ib-line px-3.5 pt-3 pb-3.5"><legend class="float-left text-[.74rem] font-bold tracking-[.08em] text-ib-muted uppercase [&+*]:clear-left">Line <span data-n>2</span></legend><div class="col-span-2"><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-desc-2">Description</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" type="text" data-field="desc" id="ib2-desc-2" value="Labour, hours"></div><div><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-qty-2">Qty</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib2-qty-2" value="2.5"></div><div><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-price-2">Price</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib2-price-2" value="88"></div><p class="col-span-2 flex justify-between tabular-nums [&>span]:text-ib-muted"><span>Amount</span> <b data-amount>£220.00</b></p><button class="inline-grid size-9 cursor-pointer place-items-center rounded-[9px] text-ib-muted transition-colors duration-150 hover:bg-[#fdecea] hover:text-[#b42318] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent absolute! top-1 right-1.5 size-8!" type="button" data-remove aria-label="Remove line 2" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li><li data-line class=""><fieldset class="relative grid min-w-0 grid-cols-2 gap-2.5 rounded-xl border border-ib-line px-3.5 pt-3 pb-3.5"><legend class="float-left text-[.74rem] font-bold tracking-[.08em] text-ib-muted uppercase [&+*]:clear-left">Line <span data-n>3</span></legend><div class="col-span-2"><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-desc-3">Description</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" type="text" data-field="desc" id="ib2-desc-3" value="Thermostatic mixing valve"></div><div><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-qty-3">Qty</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="qty" id="ib2-qty-3" value="1"></div><div><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-price-3">Price</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="price" id="ib2-price-3" value="64.5"></div><p class="col-span-2 flex justify-between tabular-nums [&>span]:text-ib-muted"><span>Amount</span> <b data-amount>£64.50</b></p><button class="inline-grid size-9 cursor-pointer place-items-center rounded-[9px] text-ib-muted transition-colors duration-150 hover:bg-[#fdecea] hover:text-[#b42318] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent absolute! top-1 right-1.5 size-8!" type="button" data-remove aria-label="Remove line 3" hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li></ol>
  <template data-line-template><li data-line class=""><fieldset class="relative grid min-w-0 grid-cols-2 gap-2.5 rounded-xl border border-ib-line px-3.5 pt-3 pb-3.5"><legend class="float-left text-[.74rem] font-bold tracking-[.08em] text-ib-muted uppercase [&+*]:clear-left">Line <span data-n>0</span></legend><div class="col-span-2"><label class="mb-[5px] block text-[.8rem] font-semibold" data-label="desc">Description</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none" type="text" data-field="desc" value=""></div><div><label class="mb-[5px] block text-[.8rem] font-semibold" data-label="qty">Qty</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="qty" value="1"></div><div><label class="mb-[5px] block text-[.8rem] font-semibold" data-label="price">Price</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" type="number" min="0" step="any" inputmode="decimal" data-field="price" value="0"></div><p class="col-span-2 flex justify-between tabular-nums [&>span]:text-ib-muted"><span>Amount</span> <b data-amount>£0.00</b></p><button class="inline-grid size-9 cursor-pointer place-items-center rounded-[9px] text-ib-muted transition-colors duration-150 hover:bg-[#fdecea] hover:text-[#b42318] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent absolute! top-1 right-1.5 size-8!" type="button" data-remove aria-label="Remove line 0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg></button></fieldset></li></template>
  <button class="mt-3 inline-flex cursor-pointer items-center gap-1.5 rounded-[10px] border-[1.5px] border-dashed border-ib-field px-3.5 py-[9px] font-bold text-ib-accent-dark transition-colors duration-150 hover:border-ib-accent hover:bg-ib-soft motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent" type="button" data-add hidden><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>Add line</button>
  <div class="mb-3.5 grid grid-cols-2 gap-3 [form:has(ol)_&]:mt-4 [form:has(ol)_&]:grid-cols-1">
    <div class=""><label class="mb-[5px] block text-[.8rem] font-semibold" for="ib2-tax">VAT %</label><input class="block w-full min-h-10 rounded-[9px] border border-ib-field bg-white px-2.5 py-2 transition-[border-color,box-shadow] duration-150 focus:border-ib-accent focus:shadow-[0_0_0_3px_rgba(15,118,110,.28)] focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ib-accent motion-reduce:transition-none text-right tabular-nums" id="ib2-tax" type="number" min="0" max="100" step="any" inputmode="decimal" value="20" data-tax></div>
  </div>
  <dl class="grid gap-1.5 rounded-xl bg-ib-soft p-4 tabular-nums"><div class="flex justify-between gap-3 [&>dt]:text-ib-muted [&>dd]:font-semibold"><dt>Subtotal</dt><dd><output data-out="subtotal">£379.50</output></dd></div><div class="flex justify-between gap-3 [&>dt]:text-ib-muted [&>dd]:font-semibold"><dt>VAT (<span data-out-rate="tax">20</span>%)</dt><dd><output data-out="tax">£75.90</output></dd></div><div class="mt-1.5 flex items-baseline justify-between gap-3 border-t border-[#c3d3d0] pt-2.5 [&>dt]:font-bold [&>dd]:text-[1.45rem] [&>dd]:font-extrabold [&>dd]:text-ib-accent-dark"><dt>Total</dt><dd><output data-out="total">£455.40</output></dd></div></dl>
  <button class="inline-flex cursor-pointer items-center justify-center gap-2 rounded-[10px] bg-ib-accent px-[18px] py-2.5 font-bold text-white transition-colors duration-150 hover:bg-ib-accent-dark motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ib-accent mt-[18px] w-full py-3" type="submit">Create invoice</button>
  <p class="sr-only" role="status" data-status></p>
</form>
</div>
CSS
@theme {
  --color-ib-accent: #0f766e;
  --color-ib-accent-dark: #115e59;
  --color-ib-ink: #13201f;
  --color-ib-muted: #4a5b59;
  --color-ib-line: #dbe5e3;
  --color-ib-field: #71817f;
  --color-ib-soft: #f1f6f5;
}
/* chevron for the currency select */
:root { --ib-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2313201f' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); }
JavaScript
// Invoice builder: add/remove lines, qty × price, discount %, tax %, totals in the chosen currency.
// Amounts are rounded per line in the currency's minor unit (cents, pence; none for yen), so they always add up.
// Front end only: nothing is saved or sent. Hooks are data attributes, so any stylesheet works.
document.querySelectorAll('[data-builder]').forEach(function (root) {
  var lines = root.querySelector('[data-lines]');
  var tpl = root.querySelector('template[data-line-template]');
  var addBtn = root.querySelector('[data-add]');
  var currency = root.querySelector('[data-currency]');
  var tax = root.querySelector('[data-tax]');
  var discount = root.querySelector('[data-discount]');
  var status = root.querySelector('[data-status]');
  if (!lines || !tpl || !currency) return;
  var uid = lines.querySelectorAll('[data-line]').length;

  function locale() { var o = currency.selectedOptions[0]; return (o && o.dataset.locale) || 'en-US'; }
  function fmt(minor, digits) {
    return new Intl.NumberFormat(locale(), { style: 'currency', currency: currency.value }).format(minor / Math.pow(10, digits));
  }
  function digits() { return new Intl.NumberFormat('en', { style: 'currency', currency: currency.value }).resolvedOptions().maximumFractionDigits; }
  function val(input, max) { var n = parseFloat(input && input.value); if (!isFinite(n) || n < 0) n = 0; return max ? Math.min(n, max) : n; }
  function out(name, text) { var el = root.querySelector('[data-out="' + name + '"]'); if (el) el.textContent = text; }

  function recalc() {
    var d = digits(), f = Math.pow(10, d), sub = 0;
    lines.querySelectorAll('[data-line]').forEach(function (line, i) {
      var amount = Math.round(val(line.querySelector('[data-field="qty"]')) * val(line.querySelector('[data-field="price"]')) * f);
      sub += amount;
      line.querySelector('[data-amount]').textContent = fmt(amount, d);
      line.querySelectorAll('[data-n]').forEach(function (n) { n.textContent = i + 1; });
      var rm = line.querySelector('[data-remove]'); if (rm) rm.setAttribute('aria-label', 'Remove line ' + (i + 1));
    });
    var off = Math.round(sub * val(discount, 100) / 100);
    var taxed = Math.round((sub - off) * val(tax, 100) / 100);
    out('subtotal', fmt(sub, d)); out('discount', off ? '−' + fmt(off, d) : fmt(0, d)); out('tax', fmt(taxed, d)); out('total', fmt(sub - off + taxed, d));
    root.querySelectorAll('[data-out-rate="discount"]').forEach(function (s) { s.textContent = val(discount, 100); });
    root.querySelectorAll('[data-out-rate="tax"]').forEach(function (s) { s.textContent = val(tax, 100); });
    return fmt(sub - off + taxed, d);
  }
  function say(text) { if (status) { status.textContent = ''; setTimeout(function () { status.textContent = text; }, 50); } }

  function addLine() {
    uid += 1;
    var frag = tpl.content.cloneNode(true), line = frag.querySelector('[data-line]');
    line.querySelectorAll('[data-field]').forEach(function (input) {
      var id = root.id + '-' + input.dataset.field + '-' + uid; input.id = id;
      var label = line.querySelector('[data-label="' + input.dataset.field + '"]'); if (label) label.htmlFor = id;
    });
    lines.appendChild(frag);
    var total = recalc();
    line.querySelector('[data-field="desc"]').focus();
    say('Line ' + lines.querySelectorAll('[data-line]').length + ' added. Total ' + total + '.');
  }

  root.addEventListener('click', function (e) {
    var rm = e.target.closest('[data-remove]');
    if (!rm) return;
    var line = rm.closest('[data-line]');
    var next = line.nextElementSibling || line.previousElementSibling;
    line.remove();
    var total = recalc();
    (next && next.matches('[data-line]') ? next.querySelector('[data-remove]') : addBtn).focus();
    say('Line removed. Total ' + total + '.');
  });
  root.addEventListener('input', recalc);
  root.addEventListener('change', function (e) {
    if (e.target.matches('[data-field="qty"],[data-field="price"],[data-tax],[data-discount],[data-currency]')) say('Total ' + recalc() + '.');
  });
  root.addEventListener('submit', function (e) { e.preventDefault(); say('Demo only: this invoice was not saved or sent. Total ' + recalc() + '.'); });
  if (addBtn) { addBtn.hidden = false; addBtn.addEventListener('click', addLine); }
  root.querySelectorAll('[data-remove]').forEach(function (b) { b.hidden = false; });
  recalc();
});

About this set

An editable invoice form that recalculates as you type. Invoice builder with editable line items has fields for the invoice number, issue and due dates, currency, client and email, a table of line items with description, quantity and unit price inputs, Add line and remove buttons, notes for the client, discount and tax percentages, and a totals panel. Compact builder for a narrow panel is the same tool for a sidebar or phone: each line is a fieldset with visible labels, and VAT is the only adjustment. A small vanilla script multiplies quantity by price for each line, rounds every amount to the currency’s minor unit so the figures always add up, applies the discount and tax, and formats everything with Intl.NumberFormat for the chosen currency and locale, so switching to euros shows 5.277,03 € and yen drops the decimals. It is front end only: the form never submits, and a polite status line says so. Without JavaScript the form still shows the starting figures, and the add and remove buttons stay hidden because they cannot work.

What’s included

  • Add and remove line items, quantity times unit price per line
  • Discount and tax percentages with a live subtotal, discount, tax and total
  • Currency select formatted with Intl.NumberFormat (USD, EUR, GBP, JPY, CAD)
  • Totals rounded in the currency minor unit so they always add up
  • Table layout for desktop and a fieldset-per-line layout for narrow panels

Accessibility

Every input has a label: visible labels for the details, and visually hidden labels such as Unit price, line 2 inside the table, renumbered after each change. Remove buttons are named Remove line 2, focus moves to the next line or the Add line button after a removal, and totals are announced in a polite status region when a value is committed. All controls show a focus outline and transitions stop under prefers-reduced-motion.

Customise it

Change --ib-accent, --ib-field and --ib-soft on .ib for colour. Add currencies as options with a data-locale attribute; the script reads the hooks data-builder, data-lines, data-line, data-field, data-add, data-remove, data-currency, data-tax, data-discount and data-out, so it works with all three stylesheets.