A rate card template you update with one click

A rate card is one page that lists what you sell and what it costs. If every price comes from one hourly rate, a rate change is one number, not a rewrite.

A rate card template is a single page with the same parts every time: each service you sell, how it is priced, the price, and the date those prices apply from. The page below calculates every price from one hourly rate.

Freelancers ask, for example on Quora, how to write a quotation mail to a client. A rate card answers the part that repeats. You link to it instead of retyping your prices into each message.

Try it. In the amber panel, press + twice. Every price moves and a note says what the rate was.

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>Rate card</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: 20px 18px 26px; }
  h1 { margin: 0; font-size: 26px; line-height: 1.2; }
  .sub { margin: 4px 0 0; font-size: 15px; color: #4b5563; }
  .stamp { display: inline-block; margin-top: 10px; padding: 3px 10px; border-radius: 999px; background: #ecfdf5; color: #065f46; font-size: 13px; font-weight: 600; }

  /* OWNER PANEL: delete this block before you send the link */
  .owner { margin: 16px 0 6px; padding: 12px 14px; border: 2px dashed #f59e0b; border-radius: 12px; background: #fffbeb; }
  .owner b { display: block; font-size: 14px; }
  .owner small { display: block; margin-top: 2px; color: #92400e; font-size: 13px; }
  .ctl { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
  .ctl button { width: 44px; height: 44px; border-radius: 10px; border: 1px solid #d1d5db; background: #fff; font-size: 22px; cursor: pointer; }
  .ctl button:active { background: #fef3c7; }
  .ctl output { min-width: 96px; text-align: center; font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
  .ctl span { font-size: 13px; color: #6b7280; }
  .code { margin-top: 10px; font: 13px/1.4 ui-monospace, Consolas, monospace; background: #fff; border: 1px solid #fde68a; border-radius: 8px; padding: 6px 8px; overflow-wrap: anywhere; }

  .group { margin: 20px 0 2px; font-size: 13px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: #6b7280; }
  .row { display: grid; grid-template-columns: 1fr auto; gap: 0 12px; padding: 12px 0; border-bottom: 1px solid #eef0f3; align-items: baseline; }
  .name { font-size: 16px; font-weight: 600; overflow-wrap: anywhere; }
  .how { grid-column: 1; font-size: 14px; color: #6b7280; }
  .price { grid-row: 1; grid-column: 2; font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
  .price small { font-size: 13px; font-weight: 400; color: #6b7280; }
  .fine { margin: 16px 0 0; font-size: 14px; color: #4b5563; }
  .fine b { color: #1f2937; }
  .changed { margin-top: 12px; font-size: 14px; color: #92400e; background: #fffbeb; border-radius: 8px; padding: 6px 10px; }
</style>
</head>
<body>
<div class="page">
  <h1>Linden Studio</h1>
  <p class="sub">Rate card for brand and web design</p>
  <span class="stamp">Rates valid from 1 November 2026</span>

  <!-- OWNER PANEL: delete this block before you send the link -->
  <div class="owner">
    <b>Owner panel: try a new hourly rate</b>
    <small>Delete this block before you send the link.</small>
    <div class="ctl">
      <button type="button" id="down" aria-label="Lower the rate by 5">&minus;</button>
      <output id="rate"></output>
      <button type="button" id="up" aria-label="Raise the rate by 5">+</button>
      <span>per hour, steps of 5</span>
    </div>
    <div class="code" id="code"></div>
  </div>
  <div class="changed" id="changed" hidden></div>

  <p class="group">Hourly and daily</p>
  <div id="timed"></div>

  <p class="group">Fixed-price work</p>
  <div id="fixed"></div>

  <p class="fine"><b>Rush work</b> (delivery within 3 working days): add <b id="rush"></b> to the price.</p>
  <p class="fine">Not included: printing, stock photos, extra revision rounds. Prices are in US dollars.</p>
</div>

<script>
  // The one number to change when your rate changes.
  const hourlyRate = 85;
  const startRate = 85;        // the rate the card started with, only used for the "was" note
  const rushPercent = 25;
  const dayDiscount = 0.9;     // a full day is 8 hours at 10% off

  // Every price below is hours x rate, rounded to the nearest 5.
  const timed = [
    { name: 'Hourly rate', how: 'Billed in 15-minute steps', hours: 1, label: '/ hour', exact: true },
    { name: 'Half day', how: '4 hours, one sitting', hours: 4 },
    { name: 'Full day', how: '8 hours, 10% off the hourly rate', hours: 8 * dayDiscount },
  ];
  const fixed = [
    { name: 'Discovery call', how: '30 minutes, brief written up', hours: 0.5 },
    { name: 'Logo design', how: '3 concepts, 2 revision rounds', hours: 8 },
    { name: 'Brand guide', how: '4 pages: colours, type, logo use', hours: 6 },
    { name: 'Website page design', how: 'One page, desktop and phone', hours: 5 },
    { name: 'Extra revision round', how: 'After the included rounds', hours: 1.5 },
  ];

  let rate = hourlyRate;
  const money = (n) => '$' + n.toLocaleString('en-US');
  const round5 = (n) => Math.round(n / 5) * 5;

  function line(s) {
    const value = s.exact ? rate : round5(s.hours * rate);
    return '<div class="row"><span class="name">' + s.name + '</span>' +
      '<span class="price">' + money(value) + (s.label ? ' <small>' + s.label + '</small>' : '') + '</span>' +
      '<span class="how">' + s.how + '</span></div>';
  }

  function draw() {
    document.getElementById('timed').innerHTML = timed.map(line).join('');
    document.getElementById('fixed').innerHTML = fixed.map(line).join('');
    document.getElementById('rate').textContent = money(rate);
    document.getElementById('rush').textContent = rushPercent + '%';
    document.getElementById('code').textContent = 'const hourlyRate = ' + rate + ';';
    const ch = document.getElementById('changed');
    ch.hidden = rate === startRate;
    ch.textContent = 'Was ' + money(startRate) + ' an hour, now ' + money(rate) + '.';
  }

  document.getElementById('up').addEventListener('click', () => { rate += 5; draw(); });
  document.getElementById('down').addEventListener('click', () => { rate = Math.max(5, rate - 5); draw(); });
  draw();
</script>
</body>
</html>
A rate card for a design studio. The amber owner panel changes the hourly rate; every other price is hours times that rate, rounded to the nearest 5.

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

What goes on a rate card

Keep the same parts in the same order, so you never forget one. The first four are the ones a client looks for.

Part What to put in it
Valid-from date The day these prices start to apply
Service name What you do, in the words a client would use
How it is priced Per hour, per day, or per project, with what is included
Price One number per line, in one currency
Rush or extras A surcharge or an add-on price, stated once
Not included What costs extra, one short sentence each

This is a price list for everyone. A price for one client and one job is a quote, covered in send a quote 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 services in the last part.

Make one self-contained HTML page: a rate card for a freelancer.
- Header: my name, what I do, and a "rates valid from" date.
- One constant at the top of the script: hourlyRate.
- Each service has a name, a short description and a number
  of hours. Its price is hours x hourlyRate, rounded to the
  nearest 5. Show "per hour" for the hourly line itself.
- A rush surcharge percent in one constant.
- A block marked OWNER PANEL with plus and minus buttons that
  change the hourly rate on screen and show the new constant.
  Keep it in one block I can delete.
- A "not included" note at the bottom.
- 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 services: (list them here with hours)
My hourly rate: (number and currency)

Then check the result before you trust it:

  1. Multiply the hours by your rate for one service and compare it with the page.
  2. Press + in the owner panel. Every priced line should move, and the hourly line should show the new rate.
  3. Search the page for TODO and replace each one with your own value.
  4. Check the currency symbol and the valid-from date.
  5. Look at it at phone width. Names and prices should both be readable.

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

One rate behind every price

The page keeps the part you judge and the part that changes in separate places. Hours are your estimate of how long a service takes. The rate is the one number that changes when you raise your prices.

Each price is hours times the hourly rate, so moving the rate from 85 to 95 moves every line.
Each price is hours times the hourly rate, so moving the rate from 85 to 95 moves every line.

That is why one click is enough. A card where every price is typed by hand needs a dozen edits, and it is easy to miss one.

Rounding is a choice. This example rounds to the nearest 5 so the card shows $130, not $127.50. Ask your AI for rounding to 1 or 10 if you prefer.

Change a rate by clicking

Some changes are not a single rate. You might raise one service and leave the rest. This second page is a working sheet: click a price, type the new one, and a changed price keeps the old one next to it.

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>Rate change 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: 560px; margin: 0 auto; background: #fff; min-height: 100vh; padding: 20px 18px 26px; }
  h1 { margin: 0; font-size: 24px; line-height: 1.2; }
  .sub { margin: 4px 0 12px; font-size: 15px; color: #4b5563; }
  .bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 8px; }
  .bar button { height: 44px; padding: 0 14px; border-radius: 10px; border: 1px solid #d1d5db; background: #fff; font-size: 15px; cursor: pointer; }
  .bar button:active { background: #eef2ff; }
  label { font-size: 14px; color: #4b5563; display: flex; align-items: center; gap: 8px; }
  input[type=date] { height: 44px; border: 1px solid #d1d5db; border-radius: 10px; padding: 0 8px; font-size: 15px; }
  .row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 2px 12px; padding: 10px 0; border-bottom: 1px solid #eef0f3; }
  .name { font-size: 16px; font-weight: 600; overflow-wrap: anywhere; }
  .unit { font-size: 14px; color: #6b7280; }
  .p { grid-row: 1 / span 2; grid-column: 2; display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
  .was { font-size: 14px; color: #9ca3af; text-decoration: line-through; font-variant-numeric: tabular-nums; }
  .price { min-width: 76px; height: 44px; border: 1px dashed #d1d5db; border-radius: 10px; background: #fff; font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; cursor: pointer; }
  .price:hover { border-color: #1d4ed8; }
  input.edit { width: 88px; height: 44px; border: 2px solid #1d4ed8; border-radius: 10px; padding: 0 8px; font-size: 18px; font-weight: 700; text-align: right; }
  .row.moved .price { background: #fffbeb; border-color: #f59e0b; border-style: solid; }
  .row.moved.off .was, .row.moved.off .tag { display: none; }
  .tag { font-size: 12px; font-weight: 700; color: #92400e; background: #fef3c7; border-radius: 999px; padding: 1px 8px; }
  .hint { font-size: 14px; color: #4b5563; margin: 4px 0 10px; }
  .note { margin-top: 18px; }
  .note h2 { margin: 0 0 6px; font-size: 16px; }
  textarea { width: 100%; min-height: 150px; padding: 10px; border: 1px solid #d1d5db; border-radius: 10px; font: 14px/1.45 ui-monospace, Consolas, monospace; color: #1f2937; background: #f9fafb; resize: vertical; }
</style>
</head>
<body>
<div class="page">
  <h1>Rate card update</h1>
  <p class="sub">Linden Studio. Click a price to type a new one.</p>

  <div class="bar">
    <button type="button" id="raise">Raise all by 5%</button>
    <button type="button" id="reset">Reset</button>
    <button type="button" id="mark" aria-pressed="true">Show what changed: on</button>
  </div>
  <div class="bar">
    <label>New rates from <input type="date" id="from" value="2026-12-01"></label>
  </div>
  <p class="hint">Changed prices turn amber and keep the old price next to them.</p>

  <div id="list"></div>

  <div class="note">
    <h2>Message to your clients</h2>
    <textarea id="msg" readonly aria-label="Message text"></textarea>
  </div>
</div>

<script>
  const items = [
    { name: 'Hourly rate', unit: 'per hour', was: 85 },
    { name: 'Day rate', unit: '8 hours', was: 610 },
    { name: 'Logo design', unit: '3 concepts', was: 680 },
    { name: 'Brand guide', unit: '4 pages', was: 510 },
    { name: 'Website page design', unit: 'one page', was: 425 },
    { name: 'Social media kit', unit: '6 templates', was: 300 },
  ];
  items.forEach((it) => { it.now = it.was; });
  let showMarks = true;

  const money = (n) => '$' + n.toLocaleString('en-US');
  const round5 = (n) => Math.round(n / 5) * 5;
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

  function dateText() {
    const v = document.getElementById('from').value;   // yyyy-mm-dd
    if (!v) return '(pick a date)';
    const [y, m, d] = v.split('-').map(Number);
    return d + ' ' + months[m - 1] + ' ' + y;
  }

  function draw() {
    const list = document.getElementById('list');
    list.innerHTML = '';
    items.forEach((it, i) => {
      const moved = it.now !== it.was;
      const row = document.createElement('div');
      row.className = 'row' + (moved ? ' moved' : '') + (showMarks ? '' : ' off');
      row.innerHTML = '<span class="name"></span><span class="unit"></span><span class="p">' +
        (moved ? '<span class="was">' + money(it.was) + '</span><span class="tag">new</span>' : '') +
        '<button type="button" class="price" aria-label="Edit price"></button></span>';
      row.querySelector('.name').textContent = it.name;
      row.querySelector('.unit').textContent = it.unit;
      const btn = row.querySelector('.price');
      btn.textContent = money(it.now);
      btn.addEventListener('click', () => edit(btn, i));
      list.appendChild(row);
    });
    writeMessage();
  }

  function edit(btn, i) {
    const input = document.createElement('input');
    input.className = 'edit'; input.type = 'number'; input.min = '0'; input.step = '5';
    input.inputMode = 'numeric'; input.value = items[i].now;
    input.setAttribute('aria-label', 'New price for ' + items[i].name);
    btn.replaceWith(input);
    input.focus(); input.select();
    let done = false;
    const commit = () => {
      if (done) return; done = true;
      const n = Number(input.value);
      if (input.value !== '' && n >= 0) items[i].now = n;
      draw();
    };
    input.addEventListener('blur', commit);
    input.addEventListener('keydown', (e) => { if (e.key === 'Enter') commit(); });
  }

  function writeMessage() {
    const changed = items.filter((it) => it.now !== it.was);
    let t;
    if (!changed.length) {
      t = 'No changes yet. Click a price, or raise them all.';
    } else {
      t = 'From ' + dateText() + ' these rates change:\n' +
        changed.map((it) => '- ' + it.name + ' (' + it.unit + '): ' + money(it.was) + ' to ' + money(it.now)).join('\n') +
        '\nEverything else stays the same. The rate card link shows the new prices.';
    }
    document.getElementById('msg').value = t;
  }

  document.getElementById('raise').addEventListener('click', () => {
    items.forEach((it) => { it.now = round5(it.now * 1.05); });
    draw();
  });
  document.getElementById('reset').addEventListener('click', () => { items.forEach((it) => { it.now = it.was; }); draw(); });
  document.getElementById('mark').addEventListener('click', (e) => {
    showMarks = !showMarks;
    e.currentTarget.textContent = 'Show what changed: ' + (showMarks ? 'on' : 'off');
    e.currentTarget.setAttribute('aria-pressed', String(showMarks));
    draw();
  });
  document.getElementById('from').addEventListener('input', writeMessage);
  draw();
</script>
</body>
</html>
A sheet for a rate change. Click a price to type a new one, or raise everything by 5%. The message to your clients writes itself.

Pick the date the new rates start. The box at the bottom turns the changes into a message you can paste into an email or chat, so clients hear about the change before they see it.

Here is what to remember about saving. The clicks on this sheet are a scratchpad. NOS keeps the owner's ticks and added rows made in View mode, and a visitor's changes are not kept. Typed values are not promised to be saved.

To make a new rate stick, change the number in the code, or ask your AI: "change hourlyRate to 95 and set the valid-from date to 1 December". Then the owner panel can go.

The same rate change on a file route and on a link route.
The same rate change on a file route and on a link route.

Delete the owner panel, then paste the finished HTML into a NOS document and choose Create share link. Put the address in your email or message. HTML to link walks through the steps.

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. They cannot edit your page, and NOS does not show you who opened it.

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

When a rate changes, edit the code. The same link shows the new version, so the address you sent last year stays right. Update the valid-from date in the same edit.

A form on the page cannot send anything to you, because sending a form to a server is blocked. Ask clients to reply to your message. Images from other sites are blocked too, so draw a logo as an inline SVG or leave it out.

Anyone with the link can open the page. Put only prices you would hand over on paper. For more on keeping one list current, read price list link and send a price list to customers.

PDF export HTML file attached Link from NOS
Raise a rate Edit, export again, send a new file Edit, send a new file Edit the code; same link
Prices Fixed text Calculated if opened in a browser Calculated on the page
Old copies One in every inbox it reached One in every inbox it reached One address

For the reasons a link holds up better than 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 price shows NaN An hours value is empty or has a letter in it Use plain numbers for hours and the rate
A price differs from your calculator The page rounds to the nearest 5 Ask for exact figures, or round on purpose
A client sees amber buttons The owner panel is still in the page Delete the block before creating the link
A new rate is gone after reopening Typed values are not what is saved Change the number in the code
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 is a rate card?

A page that lists each service you offer, how it is priced (per hour, per day or per project) and the price, with the date the prices apply from. It is the reference you point clients to, so you do not retype your prices into every message.

What is the difference between a rate card and a quote?

In this guide, a rate card is your general price list for everyone, and a quote is a price for one client and one job. A rate card says what a logo costs. A quote says what this client's logo, with these extras, costs.

Should I show prices as "from" amounts?

That is your call. A "from" price tells a client where the range starts and leaves room for a quote on bigger jobs. A flat price is simpler to read. Whichever you pick, say what is included next to the number.

Can a client change the prices on the page?

No. They cannot edit your page. If you leave the owner panel in, a visitor can click it and see other numbers on their own screen, but nothing is saved and nothing is sent to you. Remove the panel from the version you share.

How do clients know the prices are current?

Put a valid-from date at the top of the page and change it whenever a price changes. When a rate goes up, also tell your existing clients the date in a short message.

Keep reading