A freelance proposal template you can copy, fill with AI and send as a link

A proposal template is a fixed order of sections, and an AI can fill it in minutes. What decides how it looks to the client is how you send it.

A freelance proposal template is a fixed order of sections you fill in for each client: the problem, what you will do, when, what it costs, what is left out, and who you are.

You do not have to buy a designed file for that. Ask an AI to write the page, check the numbers, and send it as a link that opens on any phone.

Making the template look good is where time tends to go. One freelancer, writing about learning proposals, bought a Canva template and "spent an embarrassing amount of time on something I hoped looked polished enough to send."

The layout is the easy part. The content is what the client reads.

Here is a finished example. It is one page, written for a phone first. Tick the optional extras and the total changes.

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>Proposal - Larkspur Bakery website refresh</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.55; }
  .page { max-width: 720px; margin: 0 auto; background: #fff; padding: 24px 20px 32px; min-height: 100vh; }
  .tag { margin: 0; font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: #6b7280; }
  h1 { margin: 6px 0 4px; font-size: 28px; line-height: 1.15; }
  .meta { margin: 0 0 18px; font-size: 14px; color: #6b7280; }
  .answer { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 12px; padding: 14px 16px; margin-bottom: 6px; }
  .answer b { display: block; font-size: 22px; color: #1d4ed8; }
  .answer span { font-size: 15px; color: #374151; }
  h2 { margin: 24px 0 8px; font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: #6b7280; }
  p { margin: 0 0 8px; font-size: 16px; }
  ul { margin: 0; padding-left: 20px; }
  li { margin-bottom: 4px; font-size: 16px; }
  .steps { list-style: none; padding: 0; }
  .steps li { display: flex; gap: 12px; padding: 8px 0; border-bottom: 1px solid #eef0f3; margin: 0; }
  .steps b { min-width: 74px; color: #1d4ed8; }
  .opt { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; margin-bottom: 8px; cursor: pointer; }
  .opt input { width: 20px; height: 20px; flex: none; }
  .opt .t { flex: 1; font-size: 15px; }
  .opt .p { font-weight: 600; white-space: nowrap; }
  .total { display: flex; justify-content: space-between; align-items: baseline; margin-top: 12px; padding: 12px 14px; background: #f9fafb; border-radius: 10px; }
  .total b { font-size: 24px; }
  .small { font-size: 14px; color: #6b7280; }
  .out li { color: #4b5563; }
  .who { margin-top: 24px; padding-top: 14px; border-top: 1px solid #e5e7eb; font-size: 15px; }
  .who a { color: #1d4ed8; }
</style>
</head>
<body>
<div class="page">
  <p class="tag">Proposal</p>
  <h1>Website refresh for Larkspur Bakery</h1>
  <p class="meta">Prepared for Dana Reyes · by Mira Okafor · valid until <span id="valid"></span></p>

  <div class="answer">
    <b id="headline"></b>
    <span>One-page site with a menu, opening hours and an order form. Ready 3 weeks after you say yes.</span>
  </div>

  <h2>1. The problem</h2>
  <p>Customers find the bakery's old site on their phone, cannot read the menu without zooming, and phone you to ask if you are open.</p>

  <h2>2. What I will do</h2>
  <ul>
    <li>Design a one-page site that reads well on a phone.</li>
    <li>Put the menu, hours and map link at the top.</li>
    <li>Add a simple order-request form.</li>
    <li>Two rounds of changes after the first draft.</li>
  </ul>

  <h2>3. Timeline</h2>
  <ul class="steps">
    <li><b>Week 1</b><span>Notes, photos and menu from you. First layout from me.</span></li>
    <li><b>Week 2</b><span>First draft. You send changes.</span></li>
    <li><b>Week 3</b><span>Final version and handover.</span></li>
  </ul>

  <h2>4. Investment</h2>
  <p class="small">Base price covers everything in section 2. Tick any extra to see the new total.</p>
  <div class="opt"><span class="t">Base: one-page site</span><span class="p" id="p-base"></span></div>
  <label class="opt"><input type="checkbox" data-key="photos"><span class="t">Product photo editing (12 photos)</span><span class="p" id="p-photos"></span></label>
  <label class="opt"><input type="checkbox" data-key="menu"><span class="t">Second language for the menu</span><span class="p" id="p-menu"></span></label>
  <div class="total"><span>Total</span><b id="total"></b></div>
  <p class="small" style="margin-top:8px">A <span id="dep-pct"></span> deposit starts the work. The rest is due on handover.</p>

  <h2>5. Not included</h2>
  <ul class="out">
    <li>Logo or brand design.</li>
    <li>Domain name and hosting fees.</li>
    <li>Changes beyond the two rounds.</li>
  </ul>

  <div class="who">
    <b>About me.</b> I build small sites for local shops. Two recent examples: a florist's order page and a bike repair booking page.
    <br>Questions? mira@example.com
  </div>
</div>

<script>
  // ---- Change the numbers here. Everything else on the page follows. ----
  const PRICES = { base: 1800, photos: 240, menu: 150 };
  const DEPOSIT_PERCENT = 40;
  const VALID_UNTIL = '31 Oct 2026';
  // -----------------------------------------------------------------------

  const money = (n) => '$' + n.toLocaleString('en-US');
  const boxes = document.querySelectorAll('input[data-key]');

  document.getElementById('valid').textContent = VALID_UNTIL;
  document.getElementById('dep-pct').textContent = DEPOSIT_PERCENT + '%';
  for (const key in PRICES) document.getElementById('p-' + key).textContent = key === 'base' ? money(PRICES[key]) : '+' + money(PRICES[key]);

  function update() {
    let total = PRICES.base;
    boxes.forEach((b) => { if (b.checked) total += PRICES[b.dataset.key]; });
    document.getElementById('total').textContent = money(total);
    document.getElementById('headline').textContent = money(total) + ' fixed price';
  }
  boxes.forEach((b) => b.addEventListener('change', update));
  update();
</script>
</body>
</html>
A one-page proposal with the price on top and extras that update the total. Edit the numbers at the top of the script and it reruns.

The six parts, in the order a client reads them

The same freelancer lists six parts a proposal needs. They map onto the example above, with one change: the price and date sit in a box at the very top, because that is what the client looks for first.

  1. The problem – what is wrong now, in the client's own words.
  2. What I will do – the scope, as a short list.
  3. Timeline – three or four steps with weeks.
  4. Investment – the price, plus any optional extras.
  5. Not included – the things you will not do unless asked and paid.
  6. About me – two lines and one way to reach you.

The "not included" part feels awkward to write. It is the part that prevents the argument later. If you want more on the structure and on what survives a forward inside the client, see how to send a proposal to a client.

Make it with AI

Paste this prompt into ChatGPT, Claude or Gemini. Put your own facts at the bottom.

Write ONE self-contained HTML file: a freelance proposal, to be read on a phone.
Rules:
- Plain HTML, CSS and JavaScript in one file. No images, no external files, no form that sends data.
- One column. Add a viewport meta tag. Text at least 16px.
- Start with a box showing the price and the delivery date.
- Then these sections in order: The problem, What I will do, Timeline, Investment, Not included, About me.
- In Investment, show a base price and optional extras as tick boxes, with a total that updates.
- Put every number (prices, deposit percent, valid-until date) in one block at the top of the script, with the comment "Change the numbers here".
- If I did not give you a fact, write TODO. Do not invent clients, past work or prices.
My facts:
Client: ...
Their problem: ...
What I will do: ...
Weeks and steps: ...
Base price and extras: ...
Not included: ...
About me: ...

The line about one block of numbers matters. It means a price change later is one edit, not a hunt through the page.

What to check in the result

An AI is good at the layout and loose with facts. Read the page like the client will.

  • Every number. Prices, the total, the deposit, the dates. Compare them with your own notes.
  • Every TODO. Replace each one, or delete the line.
  • Invented details. Past clients, results and promises you did not give it.
  • The tick boxes. Tick each one and check the total.
  • The phone. Open it on your own phone before anyone else does.

If the page looks off, fixing AI-generated HTML covers the usual causes.

Paste the HTML into a NOS document and choose Create share link. HTML to link walks through it.

The result is a page anyone with the link can open in a browser, on a phone too, with no account. They can read it but not edit it.

Scripts run on the page, so the tick boxes work for the client. Their ticks are theirs alone: they are not saved and you do not see them.

Send the link in the body of your email or message, with one line saying what it is.

To update numbers: open the page in NOS, click into the code, and change the figures in the block at the top of the script.

For example, change the base price from 1800 to 2000. Save, and the same link now shows the new price. Nobody needs a new file.

Add a short dated line, such as "Price updated 12 Oct", so the client knows what changed. A second example, an hourly estimate with plus and minus buttons, works the same way.

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>Estimate - hourly project</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.5; }
  .page { max-width: 560px; margin: 0 auto; background: #fff; min-height: 100vh; padding: 22px 18px 28px; }
  h1 { margin: 0 0 4px; font-size: 24px; line-height: 1.2; }
  .sub { margin: 0 0 16px; font-size: 14px; color: #6b7280; }
  .row { display: grid; grid-template-columns: 1fr auto; gap: 4px 10px; align-items: center; padding: 12px 0; border-bottom: 1px solid #eef0f3; }
  .row .name { font-size: 16px; font-weight: 600; }
  .row .note { grid-column: 1; font-size: 14px; color: #6b7280; }
  .qty { display: flex; align-items: center; gap: 8px; grid-row: 1 / span 2; grid-column: 2; }
  .qty button { width: 40px; height: 40px; border-radius: 10px; border: 1px solid #d1d5db; background: #fff; font-size: 20px; cursor: pointer; }
  .qty button:active { background: #eef2ff; }
  .qty output { min-width: 56px; text-align: center; font-size: 15px; }
  .sum { margin-top: 16px; background: #f9fafb; border-radius: 12px; padding: 14px 16px; }
  .sum div { display: flex; justify-content: space-between; font-size: 15px; padding: 2px 0; }
  .sum .big { font-size: 22px; font-weight: 700; padding-top: 8px; margin-top: 6px; border-top: 1px solid #e5e7eb; }
  .fine { margin-top: 12px; font-size: 14px; color: #6b7280; }
</style>
</head>
<body>
<div class="page">
  <h1>Estimate: product page copy</h1>
  <p class="sub">For Harbor Tea Co. · rate <span id="rate"></span> per hour · use + and - to see the effect</p>

  <div class="row" data-key="interview">
    <span class="name">Interview call and notes</span>
    <span class="note">Fixed unit: 1 hour</span>
    <div class="qty"><button type="button" data-d="-1" aria-label="Fewer hours">-</button><output></output><button type="button" data-d="1" aria-label="More hours">+</button></div>
  </div>
  <div class="row" data-key="draft">
    <span class="name">Drafting</span>
    <span class="note">About 1 hour per product page</span>
    <div class="qty"><button type="button" data-d="-1" aria-label="Fewer hours">-</button><output></output><button type="button" data-d="1" aria-label="More hours">+</button></div>
  </div>
  <div class="row" data-key="revise">
    <span class="name">Revisions</span>
    <span class="note">One round after your comments</span>
    <div class="qty"><button type="button" data-d="-1" aria-label="Fewer hours">-</button><output></output><button type="button" data-d="1" aria-label="More hours">+</button></div>
  </div>

  <div class="sum">
    <div><span>Hours</span><span id="hours"></span></div>
    <div><span>Subtotal</span><span id="sub"></span></div>
    <div><span>Deposit to start (<span id="pct"></span>)</span><span id="dep"></span></div>
    <div class="big"><span>Estimate</span><span id="total"></span></div>
  </div>
  <p class="fine">An estimate, not a fixed price. If the hours change, I tell you before I go over.</p>
</div>

<script>
  // ---- Change the numbers here. ----
  const RATE = 60;
  const DEPOSIT_PERCENT = 30;
  const HOURS = { interview: 1, draft: 5, revise: 2 };
  // ----------------------------------

  const money = (n) => '$' + n.toLocaleString('en-US');
  document.getElementById('rate').textContent = money(RATE);
  document.getElementById('pct').textContent = DEPOSIT_PERCENT + '%';

  function render() {
    let hours = 0;
    document.querySelectorAll('.row').forEach((row) => {
      const h = HOURS[row.dataset.key];
      row.querySelector('output').textContent = h + (h === 1 ? ' hour' : ' hours');
      hours += h;
    });
    const total = hours * RATE;
    document.getElementById('hours').textContent = hours;
    document.getElementById('sub').textContent = hours + ' x ' + money(RATE);
    document.getElementById('total').textContent = money(total);
    document.getElementById('dep').textContent = money(Math.round(total * DEPOSIT_PERCENT / 100));
  }

  document.querySelectorAll('.qty button').forEach((btn) => {
    btn.addEventListener('click', () => {
      const key = btn.closest('.row').dataset.key;
      HOURS[key] = Math.max(0, HOURS[key] + Number(btn.dataset.d));
      render();
    });
  });
  render();
</script>
</body>
</html>
An hourly estimate. The client taps plus and minus to see the effect; you change rate and hours in the code.
From AI chat to a link, in four steps.
From AI chat to a link, in four steps.

Ways to send a proposal, compared

An attached file against a page sent as a link.
An attached file against a page sent as a link.
Attached file or PDF Text in the email Link to a page
Opens on a phone After a download Yes Yes, in the browser
Change after sending Send a new file Send a new email Edit the page, same link
Tick boxes and live total No No Yes
Client needs an account No No No
Forwarded inside the client Many copies Pasted text One address

For personal Gmail accounts, Google's help page says the attachment limit is 25 MB, and Gmail adds a larger file as a Drive link instead (as of October 2026).

Work and school accounts can have limits set by an administrator. A one-page proposal sits far below that, so size is rarely the problem. Changing it later is. See email vs link for documents.

What the client sees on a phone: price and date first, then the rest in one column.
What the client sees on a phone: price and date first, then the rest in one column.

When it does not work

What you see Cause Fix
The page shows raw code in the chat You copied the chat text, not the code block Copy only the code, from the first doctype line to the last html tag
A picture or logo is missing Images from other sites are blocked Ask for no images, or a logo drawn in SVG
The total does not change The AI left the script out or broke it Ask: "Make the tick boxes update the total"
A contact form does nothing Forms cannot send to a server from the link Write your email address as plain text
The client sees the old price They have an old copy, or an old file Send only the link, never the file
The text is tiny on a phone No viewport meta tag Ask the AI to add one, and a 16px base size

A proposal is one document in a chain. After it is accepted you will likely send a quote, a contract and an invoice. Each can be a link in the same way.

For the wider picture, the proposal link guide covers writing for the person who was not on the call.

Questions people ask

What should a freelance proposal include?

Six parts cover it: the client's problem in their words, the scope of work, the timeline, the price, what is not included, and a short line about you. One freelancer's write-up on proposals uses this same list. Put the price and the date near the top so the client sees them first.

Do I need a designed template from Canva or a template store?

No. A proposal is mostly an order of sections and the right numbers. An AI can write a clean one-page layout for you, and you check the words and figures. Spend your time on the scope and the price, not on fonts.

Can the client change the numbers on my page?

They can tick the optional extras and watch the total move on their own screen. That change is not saved and you do not see it. Only you change the page, by editing the code in your NOS document, and the link stays the same.

How do I change a price after I have sent the link?

Open the page in NOS, find the block of numbers at the top of the script, type the new figure and save. The same link then shows the new price. Add a dated line such as "Price updated 12 Oct" so the client knows.

Will the page work if the AI used images or a contact form?

Ask for no images and no form that sends data. Images loaded from other sites and forms that post to a server are blocked on a NOS share link. Put your email address on the page as plain text so the client can copy it.

Keep reading