A freelance quote template where the totals follow your clicks

A quote is a short list of line items and a total. If the total updates when you tick or click, a client's change takes seconds, not a new PDF.

A freelance quote template is a page with the same parts every time: who it is for, line items with a quantity and a rate, a subtotal, a total, and how long the price holds. The page below does the sums for you.

When a client asks to drop one item or add another, you click, read the new total and send. You do not export a new PDF.

Try it. Untick the logo concepts, press + on the revision rounds, tick the brand guide, then add a line. The totals follow every click.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quote Q-0042</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; background: #f3f4f6; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #1f2937; line-height: 1.45; }
  .page { max-width: 560px; margin: 0 auto; background: #fff; min-height: 100vh; padding: 22px 18px 26px; }
  h1 { margin: 0; font-size: 24px; line-height: 1.2; }
  .meta { margin: 2px 0 0; font-size: 14px; color: #6b7280; }
  .head { padding-bottom: 14px; border-bottom: 2px solid #1f2937; margin-bottom: 4px; }
  .hint { margin: 10px 0 0; font-size: 14px; color: #4b5563; }
  .row { display: grid; grid-template-columns: 30px 1fr auto; grid-template-areas: "tick what amt" ". step ."; gap: 2px 10px; padding: 12px 0; border-bottom: 1px solid #eef0f3; align-items: center; }
  .row input[type=checkbox] { grid-area: tick; width: 22px; height: 22px; margin: 0; accent-color: #1d4ed8; }
  .what { grid-area: what; min-width: 0; }
  .name { display: block; font-size: 16px; font-weight: 600; overflow-wrap: anywhere; }
  .calc { display: block; font-size: 14px; color: #6b7280; }
  .amt { grid-area: amt; font-size: 16px; font-weight: 600; text-align: right; font-variant-numeric: tabular-nums; }
  .step { grid-area: step; display: flex; align-items: center; gap: 8px; margin-top: 6px; }
  .step button { width: 40px; height: 40px; border-radius: 10px; border: 1px solid #d1d5db; background: #fff; font-size: 20px; cursor: pointer; }
  .step button:active { background: #eef2ff; }
  .step .q { min-width: 70px; text-align: center; font-size: 14px; font-weight: 400; }
  .row.off .name, .row.off .calc, .row.off .amt, .row.off .q { opacity: .4; text-decoration: line-through; }
  #add { margin-top: 12px; height: 44px; padding: 0 16px; border: 1px dashed #9ca3af; border-radius: 10px; background: #fff; font-size: 15px; cursor: pointer; color: #1d4ed8; }
  .sum { margin-top: 18px; background: #f9fafb; border-radius: 12px; padding: 14px 16px; }
  .sum div { display: flex; justify-content: space-between; font-size: 15px; padding: 2px 0; font-variant-numeric: tabular-nums; }
  .sum .big { font-size: 22px; font-weight: 700; padding-top: 8px; margin-top: 6px; border-top: 1px solid #e5e7eb; }
  .fine { margin: 14px 0 0; font-size: 14px; color: #6b7280; }
</style>
</head>
<body>
<div class="page">
  <div class="head">
    <h1>Quote Q-0042</h1>
    <p class="meta">Linden Studio, for Marlow Bakery</p>
    <p class="meta">Valid for 14 days from the day it was sent</p>
  </div>
  <p class="hint">Tick a line in or out. Use + and - to change a quantity.</p>

  <div id="lines">
    <div class="row" data-rate="70" data-qty="1" data-unit="hour">
      <input type="checkbox" checked aria-label="Include this line">
      <div class="what"><span class="name">Kickoff call and brief</span><span class="calc"></span></div>
      <span class="amt"></span>
      <div class="step"><button type="button" data-d="-1" aria-label="Less">-</button><span class="q"></span><button type="button" data-d="1" aria-label="More">+</button></div>
    </div>
    <div class="row" data-rate="150" data-qty="3" data-unit="concept">
      <input type="checkbox" checked aria-label="Include this line">
      <div class="what"><span class="name">Logo concepts</span><span class="calc"></span></div>
      <span class="amt"></span>
      <div class="step"><button type="button" data-d="-1" aria-label="Less">-</button><span class="q"></span><button type="button" data-d="1" aria-label="More">+</button></div>
    </div>
    <div class="row" data-rate="80" data-qty="2" data-unit="round">
      <input type="checkbox" checked aria-label="Include this line">
      <div class="what"><span class="name">Revision rounds</span><span class="calc"></span></div>
      <span class="amt"></span>
      <div class="step"><button type="button" data-d="-1" aria-label="Less">-</button><span class="q"></span><button type="button" data-d="1" aria-label="More">+</button></div>
    </div>
    <div class="row" data-rate="240" data-qty="1" data-unit="guide">
      <input type="checkbox" aria-label="Include this line">
      <div class="what"><span class="name">Brand guide, 4 pages (optional)</span><span class="calc"></span></div>
      <span class="amt"></span>
      <div class="step"><button type="button" data-d="-1" aria-label="Less">-</button><span class="q"></span><button type="button" data-d="1" aria-label="More">+</button></div>
    </div>
  </div>
  <button id="add" type="button">+ Add a line</button>

  <div class="sum">
    <div><span>Subtotal</span><span id="sub"></span></div>
    <div id="taxrow"><span>Tax (<span id="taxpct"></span>%)</span><span id="tax"></span></div>
    <div class="big"><span>Total</span><span id="total"></span></div>
    <div><span>Deposit to start (<span id="deppct"></span>%)</span><span id="dep"></span></div>
  </div>
  <p class="fine">Not included: printing, stock photos, extra revision rounds. Reply to the message this link came in to accept.</p>
</div>

<script>
  // ---- Change these two numbers. Set TAX_PERCENT to 0 to hide the tax line. ----
  const TAX_PERCENT = 8;
  const DEPOSIT_PERCENT = 30;
  // ------------------------------------------------------------------------------

  const lines = document.getElementById('lines');
  const template = lines.firstElementChild.cloneNode(true);
  const cents = (n) => Math.round(n * 100) / 100;
  const money = (n) => '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });

  function update() {
    let sub = 0;
    lines.querySelectorAll('.row').forEach((row) => {
      const qty = Number(row.dataset.qty), rate = Number(row.dataset.rate);
      const amount = cents(qty * rate);
      const on = row.querySelector('input[type=checkbox]').checked;
      row.classList.toggle('off', !on);
      row.querySelector('.q').textContent = qty + ' ' + row.dataset.unit + (qty === 1 ? '' : 's');
      row.querySelector('.calc').textContent = money(rate) + ' per ' + row.dataset.unit;
      row.querySelector('.amt').textContent = money(amount);
      if (on) sub += amount;
    });
    const tax = cents(sub * TAX_PERCENT / 100);
    document.getElementById('sub').textContent = money(sub);
    document.getElementById('taxpct').textContent = TAX_PERCENT;
    document.getElementById('tax').textContent = money(tax);
    document.getElementById('taxrow').hidden = TAX_PERCENT === 0;
    document.getElementById('total').textContent = money(cents(sub + tax));
    document.getElementById('deppct').textContent = DEPOSIT_PERCENT;
    document.getElementById('dep').textContent = money(cents((sub + tax) * DEPOSIT_PERCENT / 100));
  }

  lines.addEventListener('click', (e) => {
    const btn = e.target.closest('button[data-d]');
    if (!btn) return;
    const row = btn.closest('.row');
    row.dataset.qty = Math.max(0, Number(row.dataset.qty) + Number(btn.dataset.d));
    update();
  });
  lines.addEventListener('change', update);

  document.getElementById('add').addEventListener('click', () => {
    const row = template.cloneNode(true);
    row.dataset.qty = 1;
    row.dataset.rate = 50;
    row.dataset.unit = 'item';
    row.querySelector('input[type=checkbox]').checked = true;
    const name = row.querySelector('.name');
    name.textContent = 'New item';
    name.contentEditable = 'true';
    lines.appendChild(row);
    update();
  });

  update();
</script>
</body>
</html>
A quote with line items. Tick a line in or out, change a quantity with plus and minus, add a line. Subtotal, tax, total and deposit update at once.

The studio, the client and every price are made up. Replace them with your own.

What goes in a quote

Freelancers ask this in plain words, for example on Quora: how do you send a new client an estimate and invoice? This guide covers the quote half. Keep the same parts in the same order, so you never forget one.

Part What to put in it
Header Quote number, your name, the client's name, the date
Line items One row per deliverable: description, quantity, unit, rate, amount
Optional lines Extras the client can take or leave, shown as tick boxes
Subtotal and tax Tax on its own line only if you charge it; check the rule where you work
Total and deposit One number the client can say aloud, and the deposit if you ask for one
Validity How long the price holds, for example 14 days from sending
Not included What would cost extra, one short sentence each

A quote and an invoice are different documents. For the second one, see how to send an invoice to a client.

Make it with AI

An AI chat can write this page in one reply. Paste this prompt into ChatGPT, Claude or Gemini, and put your real lines in the last part.

Make one self-contained HTML page: a quote for a client.
- Header: quote number, my name, client name, date.
- A list of line items: description, quantity, unit, rate, amount.
  Keep the numbers in plain data attributes so I can edit them.
- Each line has a tick box to include it. Optional lines start unticked.
- Plus and minus buttons change a quantity. Never below zero.
- An "Add a line" button.
- Subtotal, a tax line (rate in one constant, hidden at 0),
  total, and a deposit (percent in one constant).
- Show "valid for 14 days" and a short "not included" note.
- No external libraries, fonts or images. No localStorage.
- It must work on a phone, 390 px wide.
- Do not invent names, prices or dates. Write TODO where I gave none.
My lines: (list them here with quantity and rate)

Then check the result before you trust it:

  1. Do the sums for one row and for the total on a calculator. A page can look right and add up wrong.
  2. Press + once and tick one line. The total should move by exactly that amount.
  3. Search the page for TODO and replace each one with your own value.
  4. Check the currency symbol, and set the tax rate yourself.
  5. Look at it at phone width. Every line and button should be readable.

If the reply shows as code instead of a page, see AI HTML not rendering.

Fix it by clicking, before you send

Clicks cover a quantity and an optional line. A rate or a description needs typing. This second page is a working sheet: type into the cells, remove a line with the cross, and add a discount.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quote sheet</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; background: #f3f4f6; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; color: #1f2937; line-height: 1.45; }
  .page { max-width: 600px; margin: 0 auto; background: #fff; min-height: 100vh; padding: 20px 16px 26px; }
  h1 { margin: 0 0 2px; font-size: 22px; }
  .sub { margin: 0 0 14px; font-size: 14px; color: #6b7280; }
  .row { display: grid; grid-template-columns: 64px 1fr 1fr 38px; grid-template-areas: "desc desc desc desc" "qty rate amt del"; gap: 6px 8px; align-items: center; padding: 10px 0; border-bottom: 1px solid #eef0f3; }
  .row .desc { grid-area: desc; }
  .row .qty { grid-area: qty; }
  .row .rate { grid-area: rate; }
  .row .amt { grid-area: amt; text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; font-size: 15px; }
  .row .del { grid-area: del; }
  input[type=text], input[type=number] { width: 100%; height: 40px; padding: 0 10px; border: 1px solid #d1d5db; border-radius: 8px; font: inherit; font-size: 15px; background: #fff; }
  .del, #add { border: 1px solid #d1d5db; background: #fff; border-radius: 8px; cursor: pointer; font: inherit; font-size: 15px; }
  .del { height: 40px; color: #b91c1c; }
  #add { margin-top: 12px; height: 42px; padding: 0 14px; color: #1d4ed8; border-style: dashed; }
  .opts { display: flex; gap: 12px; margin-top: 16px; }
  .opts label { flex: 1; font-size: 14px; color: #4b5563; }
  .opts input { margin-top: 4px; }
  .sum { margin-top: 14px; background: #f9fafb; border-radius: 12px; padding: 12px 16px; }
  .sum div { display: flex; justify-content: space-between; font-size: 15px; padding: 2px 0; font-variant-numeric: tabular-nums; }
  .sum .big { font-size: 21px; font-weight: 700; padding-top: 8px; margin-top: 6px; border-top: 1px solid #e5e7eb; }
  h2 { margin: 18px 0 6px; font-size: 15px; }
  pre { margin: 0; padding: 12px; background: #111827; color: #e5e7eb; border-radius: 10px; font: 13px/1.5 ui-monospace, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<div class="page">
  <h1>Quote sheet</h1>
  <p class="sub">Type a description, a quantity and a rate. The amounts and the text below follow.</p>

  <div id="lines"></div>
  <button id="add" type="button">+ Add a line</button>

  <div class="opts">
    <label>Discount %<input id="disc" type="number" min="0" max="100" step="1" value="10"></label>
    <label>Deposit %<input id="depin" type="number" min="0" max="100" step="5" value="25"></label>
  </div>

  <div class="sum">
    <div><span>Subtotal</span><span id="sub"></span></div>
    <div><span>Discount</span><span id="discamt"></span></div>
    <div class="big"><span>Total</span><span id="total"></span></div>
    <div><span>Deposit</span><span id="dep"></span></div>
  </div>

  <h2>The same quote as plain text, for an email or a chat message</h2>
  <pre id="text"></pre>
</div>

<script>
  // Starting lines. Replace them with your own.
  const START = [
    ['Menu photography, half day', 1, 320],
    ['Photo editing', 12, 12],
    ['Usage licence, web and print', 1, 150],
  ];

  const lines = document.getElementById('lines');
  const cents = (n) => Math.round(n * 100) / 100;
  const money = (n) => '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
  const num = (v) => { const n = parseFloat(v); return isNaN(n) || n < 0 ? 0 : n; };

  function addRow(desc, qty, rate) {
    const row = document.createElement('div');
    row.className = 'row';
    row.innerHTML =
      '<input class="desc" type="text" aria-label="Description">' +
      '<input class="qty" type="number" min="0" step="1" aria-label="Quantity">' +
      '<input class="rate" type="number" min="0" step="1" aria-label="Rate">' +
      '<span class="amt"></span>' +
      '<button class="del" type="button" aria-label="Remove this line">&times;</button>';
    row.querySelector('.desc').value = desc;
    row.querySelector('.qty').value = qty;
    row.querySelector('.rate').value = rate;
    lines.appendChild(row);
  }

  function update() {
    let sub = 0, text = [];
    lines.querySelectorAll('.row').forEach((row) => {
      const q = num(row.querySelector('.qty').value), r = num(row.querySelector('.rate').value);
      const amount = cents(q * r);
      row.querySelector('.amt').textContent = money(amount);
      sub += amount;
      text.push(row.querySelector('.desc').value + ': ' + q + ' x ' + money(r) + ' = ' + money(amount));
    });
    const d = Math.min(num(document.getElementById('disc').value), 100);
    const discount = cents(sub * d / 100);
    const total = cents(sub - discount);
    const dep = cents(total * Math.min(num(document.getElementById('depin').value), 100) / 100);
    document.getElementById('sub').textContent = money(sub);
    document.getElementById('discamt').textContent = '-' + money(discount);
    document.getElementById('total').textContent = money(total);
    document.getElementById('dep').textContent = money(dep);
    text.push('Subtotal: ' + money(sub), 'Discount (' + d + '%): -' + money(discount), 'Total: ' + money(total), 'Deposit to start: ' + money(dep));
    document.getElementById('text').textContent = text.join('\n');
  }

  lines.addEventListener('input', update);
  lines.addEventListener('click', (e) => {
    const del = e.target.closest('.del');
    if (del) { del.closest('.row').remove(); update(); }
  });
  document.getElementById('disc').addEventListener('input', update);
  document.getElementById('depin').addEventListener('input', update);
  document.getElementById('add').addEventListener('click', () => { addRow('New item', 1, 0); update(); });

  START.forEach((s) => addRow(s[0], s[1], s[2]));
  update();
</script>
</body>
</html>
A fill-in sheet. Type a description, a quantity and a rate. Amounts, discount, total, deposit and a plain-text version update as you type.

The plain-text box at the bottom is for a message where a link is not welcome. Copy it into the body of an email.

The same client request on a file route and on a link route.
The same client request on a file route and on a link route.

Here is what NOS keeps. When you, the owner, work in View mode, your ticks and the rows you add are saved. A visitor's are not.

Treat typed values as a scratchpad. To keep a number, change it in the code, or ask your AI to change it, before you create the link.

Once the sums pass your check, paste the finished HTML into a NOS document and choose Create share link. Put the address in your email or chat message. HTML to link walks through the steps.

From the AI prompt to a link in your message.
From the AI prompt to a link in your message.

The client opens it in a browser, on a phone or a laptop, with no account. The page renders as written and its scripts run, so the totals work. They cannot edit your page.

What the client sees on a phone, and what stays yours.
What the client sees on a phone, and what stays yours.

If you fix the quote later, change the code and the same link shows the new version. Add one line near the top saying what changed and when, so the client knows the figures moved.

An "Accept" button cannot send anything to you, because sending a form to a server is blocked. Ask for a reply to your message instead. Anyone with the link can open the page, so leave out what you would not hand over on paper.

For the wording and timing of the message around the link, read send a quote to a client and quote link.

PDF export HTML file attached Link from NOS
Fix a line Edit, export again, send a new file Edit, send a new file Edit the code; same link
Totals Fixed text Calculated if the file is opened in a browser Calculated on the page
Client tries an optional line No Only after opening the file Yes, not saved
Versions in the inbox One per fix One per fix One address

The point is the second row. A PDF holds numbers as fixed text. A page can hold the rule that makes them. For more on why a link beats a file, see why links beat attachments.

When it does not work

What you see Cause Fix
The preview shows code, not a page The chat text or fences came along with the HTML Paste only the HTML, from the first tag to the last
A total shows NaN or $0.00 A quantity or rate is empty or has a symbol in it Use plain numbers; put the symbol in the display code
The total differs from your calculator Tax or discount is applied in a different order Decide the order, then tell the AI
A tick or added row is gone for the client A visitor's changes are not saved Make the change yourself in View mode, or in the code
A typed number is gone after reopening Typed values are not what is saved Change the number in the code
An Accept button does nothing Sending a form to a server is blocked Ask for a reply by email
A logo does not show Images from other sites are blocked Draw it as an inline SVG, or leave it out

Questions people ask

What should a freelance quote include?

Seven parts cover it: a header with the quote number and both names, line items with quantity and rate, optional lines, a subtotal with tax if you charge it, a total, how long the price holds, and what is not included. Keep the order the same on every quote.

What is the difference between a quote and an invoice?

In this guide, a quote is a price you offer before work starts, and an invoice asks for payment afterwards. Some people use "estimate" for a rougher figure and some use it for the same document as a quote. Use the word your client uses.

Can my client change the quantities on the page?

They can tick lines and click the buttons to see what a total would be, because the scripts on the page run. None of it is saved on your page, and nothing is sent to you. Ask them to reply with what they want.

Can I see whether the client opened the quote?

No. NOS does not show who opened a page. If you need to know where you stand, say in your message when you will follow up, and ask for a short reply.

What if a client asks for a PDF?

You can still make one for a client or an accountant who needs a file. The link is for reading the quote on any screen and for fixing it without sending a second document.

Keep reading