Build a Client Price Quotation Tool
Build a browser-based price quotation tool with real-time calculations.
Introduction
30 Second Summary
Every freelancer, small business owner, or consultant has faced the same task: a client asks for a price quote, and you end up wrestling with spreadsheets or word processors that were never designed for the job.
In this project, you will build a professional price quotation tool that runs entirely in your web browser. Using just HTML, CSS, and JavaScript, you will create a fully interactive document where you enter company details, add line items, and watch totals calculate in real time.
What You'll Build
You'll open a single HTML file, type in your company info and line items, watch every total calculate in real time, and hit Print to hand your client a polished PDF quotation.
By the end of this project, you'll have:
- A fully editable quotation document where you type company details, client info, and quote metadata directly in the browser.
- A dynamic line items table where you add and remove rows, and the Amount, Subtotal, VAT, and Grand Total update instantly as you type.
- A one-click print feature that strips away all editing controls and produces a clean professional PDF you can send straight to a client.
- Secret Mission: Add save and load buttons that store your entire quotation draft in the browser, so your data survives a page refresh.
Are there any prerequisites?
No prior coding experience is required. This project walks you through installing Visual Studio Code and creating every file from scratch.
Before We Start
Before writing any code, take a moment to think about what you are building and why it matters to you.
Set Up Your Coding Environment
Before building your quotation tool, you need two things: a code editor to write your files in, and a project folder to keep everything organized.
Visual Studio Code is a free code editor that makes writing HTML, CSS, and JavaScript easy. In this step, you will install it and create a project folder with three files for your quotation tool's structure, styles, and behavior.
In this step, get ready to:
- Install Visual Studio Code.
- Create the quotation-tool project folder with index.html, style.css, and script.js.
- Open the folder in VS Code and verify your setup.
Install Visual Studio Code
- Open your web browser and go to the Visual Studio Code download page.
- Click the download button for your operating system (Windows, macOS, or Linux).
- Run the downloaded installer and follow the prompts to complete the installation.
Choose the User Setup installer
On Windows, VS Code offers a User Setup installer that does not require administrator permissions. It is the recommended option for most users.
VS Code won't install?
Make sure you downloaded the correct version for your operating system. If the installer asks for administrator permissions on Windows, try the User Setup installer instead of the System installer.
Help me troubleshoot my VS Code installation.
Create the project folder and files
Your quotation tool needs three separate files to keep its structure, styling, and logic organized.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop and create the project folder by running these commands:
cd ~/Desktop
mkdir quotation-tool
cd quotation-tool
- Create three empty project files by running the command for your operating system:
macOS
touch index.html style.css script.js
Windows
New-Item index.html -ItemType File
New-Item style.css -ItemType File
New-Item script.js -ItemType File
- Confirm the files exist by running:
ls
You should see index.html, style.css, and script.js listed in the output.
Don't see all three files?
Run pwd to check your current location. You should be inside the quotation-tool folder on your Desktop.
Check for typos in the filenames. They must be exactly index.html, style.css, and script.js.
Help me create my project files on the Desktop.
Open the folder in VS Code
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- In the file dialog, click Desktop in the left sidebar.
- Select the quotation-tool folder.
- Click Open (macOS) or Select Folder (Windows).
Before you look at the sidebar, what three files do you expect to see listed in the Explorer panel?
You should see index.html, style.css, and script.js listed in the Explorer sidebar on the left.
Don't see the Explorer sidebar?
Click the top icon in the left sidebar (it looks like two overlapping files) to open the Explorer panel.
If you see an empty panel with no files, you may have opened your entire Desktop instead of the quotation-tool folder. Click File in the top menu, then click Open Folder again and select the correct folder.
Help me find my project files in VS Code.
Your coding environment is ready. In the next step, you will build the HTML skeleton that defines every section of your quotation document.
Build the Quotation Structure
You have VS Code open with three empty files. Right now there is nothing for a browser to display, nothing for CSS to style, and nothing for JavaScript to interact with.
In this step, you will build the full HTML skeleton for your quotation document. Every section of the page, from the company header down to the terms and conditions, starts here.
In this step, get ready to:
- Set up the HTML document skeleton with links to your stylesheet and script.
- Add a company header and client detail sections with editable fields.
- Build a line items table, totals section, terms and conditions, and action buttons.
Set up the document skeleton
Every HTML page starts with a standard skeleton that tells the browser what language you are writing in, what character set to use, and where to find your CSS and JavaScript files.
- Click on index.html in the VS Code Explorer sidebar to open it.
- Copy the following HTML skeleton into the file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Price Quotation</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="quotation">
</div>
<script src="script.js"></script>
</body>
</html>
What does this code do?
- <!DOCTYPE html> tells the browser this is a modern HTML5 page.
- The <head> section sets the page title and links to style.css so the browser knows where your styles live.
- The empty <div class="quotation"> is the container where all of the quotation content will go.
- The <script> tag at the bottom links to script.js so the browser loads your JavaScript after the page content.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Open your quotation-tool folder on your Desktop.
- Double-click index.html to open it in your browser (Chrome or Edge).
You should see a completely blank white page. That is expected. The skeleton is working, but you have not added any visible content yet.
See a file download instead of a web page?
- Make sure the file is saved with the exact name index.html (not index.html.txt). Check the file extension in your Explorer sidebar.
- Try right-clicking the file, selecting Open with, and choosing Chrome or Edge.
Still stuck? Help me figure out why my HTML file is not opening in the browser.
Add the company header and client details
The top of a quotation document needs the company's identity and the client's information. You will build both of these sections using a mix of directly editable text and input fields.
- Switch back to index.html in VS Code.
- Find the empty <div class="quotation"> container.
- Add the company header inside the container by pasting this code between the opening and closing <div class="quotation"> tags:
<header class="quotation-header">
<div class="company-info">
<h1 contenteditable="true">Your Company Name</h1>
<p contenteditable="true">123 Business Street, City, Country</p>
<p contenteditable="true">Phone: +1 234 567 890</p>
<p contenteditable="true">Email: info@company.com</p>
<p contenteditable="true">TRN: 1234567890</p>
</div>
<div class="quotation-title">
<h2>QUOTATION</h2>
</div>
</header>
What is contenteditable?
The contenteditable="true" attribute turns a regular HTML element into something the user can click and type into directly. Unlike an <input> field, it looks like normal text on the page until you click it.
The company details use contenteditable because they are free-form text that should blend into the document. Client details will use <input> fields instead, because JavaScript needs to read those values for calculations later.
- Save index.html and refresh your browser tab.
You should see "Your Company Name" as a heading, the address and contact details below it, and "QUOTATION" on the right. It looks plain without any styling, but the content is there.
Don't see any text on the page?
- Make sure you pasted the header code inside the <div class="quotation"> container, not outside it.
- Check that you saved the file before refreshing the browser.
Need more help? Help me debug why my HTML header is not showing in the browser.
Next, you need two side-by-side sections below the header: one for the client's contact details and one for the quotation's dates and reference number.
- Add the client details section directly below the closing </header> tag by pasting this code:
<div class="quotation-meta">
<div class="meta-left">
<table class="meta-table">
<tr>
<td class="meta-label">To:</td>
<td><input type="text" id="clientName" placeholder="Client Name"></td>
</tr>
<tr>
<td class="meta-label">Company:</td>
<td><input type="text" id="clientCompany" placeholder="Client Company"></td>
</tr>
<tr>
<td class="meta-label">Address:</td>
<td><input type="text" id="clientAddress" placeholder="Client Address"></td>
</tr>
<tr>
<td class="meta-label">Phone:</td>
<td><input type="text" id="clientPhone" placeholder="Client Phone"></td>
</tr>
<tr>
<td class="meta-label">Email:</td>
<td><input type="text" id="clientEmail" placeholder="Client Email"></td>
</tr>
</table>
</div>
What does this code do?
- The quotation-meta wrapper will hold two columns side by side once CSS is applied.
- Each row in the table pairs a label (like "To:" or "Company:") with an <input> field where the user types the client's information.
- Every input has a unique id like clientName so JavaScript can read these values later.
- Now add the quote metadata section directly below the closing </div> of meta-left by pasting this code:
<div class="meta-right">
<table class="meta-table">
<tr>
<td class="meta-label">Quote #:</td>
<td><input type="text" id="quoteNumber" placeholder="QT-001"></td>
</tr>
<tr>
<td class="meta-label">Created Date:</td>
<td><input type="date" id="createdDate"></td>
</tr>
<tr>
<td class="meta-label">Revised Date:</td>
<td><input type="date" id="revisedDate"></td>
</tr>
<tr>
<td class="meta-label">Valid Until:</td>
<td><input type="date" id="validUntil"></td>
</tr>
</table>
</div>
</div>
What does this code do?
- The meta-right section holds the quotation's reference number and key dates.
- The type="date" inputs give the user a built-in date picker in the browser instead of requiring them to type a date manually.
- The final </div> closes the quotation-meta wrapper that holds both the left and right columns together.
- Save index.html and refresh your browser.
You should now see input fields for the client details (To, Company, Address, Phone, Email) and the quote metadata (Quote #, Created Date, Revised Date, Valid Until) below the company header. They will be stacked vertically for now since CSS has not been applied yet.
Missing some input fields?
- Check that you pasted the meta-right block directly after the closing </div> of meta-left, not inside it.
- Make sure both blocks sit inside the <div class="quotation-meta"> wrapper.
Need help? Help me figure out why some of my quotation meta fields are missing.
Build the line items table and footer sections
The heart of a quotation is the line items table where you list each product or service with its quantity and price. Below the table, you need totals, terms, and action buttons.
- Add the line items table directly below the closing </div> of quotation-meta by pasting this code:
<table class="items-table">
<thead>
<tr>
<th>Sr #</th>
<th>Brand</th>
<th>Item ID</th>
<th>Description</th>
<th>UOM</th>
<th>QTY</th>
<th>Unit Price</th>
<th>Amount</th>
<th class="no-print">Action</th>
</tr>
</thead>
<tbody id="itemsBody">
<tr>
<td class="row-number">1</td>
<td><input type="text" placeholder="Brand"></td>
<td><input type="text" placeholder="Item ID"></td>
<td><input type="text" placeholder="Description" class="desc-input"></td>
<td><input type="text" placeholder="UOM" class="small-input"></td>
<td><input type="number" class="qty-input" value="0" min="0"></td>
<td><input type="number" class="price-input" value="0" min="0" step="0.01"></td>
<td class="amount">0.00</td>
<td class="no-print"><button class="remove-btn" onclick="removeRow(this)">Remove</button></td>
</tr>
</tbody>
</table>
What does this table do?
- The <thead> defines the column headers: Sr #, Brand, Item ID, Description, UOM (unit of measure), QTY, Unit Price, Amount, and Action.
- The <tbody> with id="itemsBody" contains one starter row. JavaScript will add new rows here later.
- The Amount cell (class="amount") is plain text, not an input. JavaScript will calculate and fill this value automatically.
- The no-print class on the Action column marks it for hiding when the document is printed.
- Save index.html and refresh your browser.
You should see a table with column headers and one row of input fields, plus a Remove button. The table is unstyled, so columns may look uneven. That is normal.
- Add the Add Item button and totals section directly below the closing </table> tag by pasting this code:
<div class="controls no-print">
<button id="addRowBtn" onclick="addRow()">+ Add Item</button>
</div>
<div class="totals">
<table class="totals-table">
<tr>
<td class="totals-label">Subtotal:</td>
<td class="totals-value" id="subtotal">0.00</td>
</tr>
<tr>
<td class="totals-label">VAT (5%):</td>
<td class="totals-value" id="vat">0.00</td>
</tr>
<tr class="grand-total-row">
<td class="totals-label">Grand Total:</td>
<td class="totals-value" id="grandTotal">0.00</td>
</tr>
</table>
</div>
What does this code do?
- The addRowBtn button calls an addRow() function (which you will write in a later step) to insert new line items.
- The totals table has cells with IDs like subtotal, vat, and grandTotal so JavaScript can update these numbers whenever quantities or prices change.
- Both the button and the controls wrapper have the no-print class so they are hidden from printed output.
- Add the terms and conditions section and the print button directly below the closing </div> of the totals section by pasting this code:
<div class="terms">
<h3>Terms & Conditions</h3>
<div contenteditable="true">
<p>1. Prices are valid for 30 days from the date of this quotation.</p>
<p>2. Payment terms: 50% advance, 50% upon delivery.</p>
<p>3. Delivery time: To be confirmed upon order confirmation.</p>
<p>4. All prices are in USD unless otherwise stated.</p>
</div>
</div>
<div class="controls no-print">
<button id="printBtn" onclick="window.print()">Print / Save as PDF</button>
</div>
What does this code do?
- The terms section uses contenteditable="true" so the user can click the terms text and edit it directly in the browser.
- The print button calls window.print(), a built-in browser function that opens the print dialog.
Before you save and check the browser, what do you think the page will look like? Every section of the quotation is now in the HTML, but there is no CSS yet.
- Save index.html and refresh your browser.
You should see everything on the page now: the company header, client detail fields, quote metadata fields, a table with column headers and one data row, an "+ Add Item" button, totals showing 0.00, the terms and conditions text, and a "Print / Save as PDF" button. It looks plain and messy because there is no styling yet. That is exactly right.
Page looks broken or sections are missing?
- Check that every opening tag has a matching closing tag. A missing </div>, </table>, or </tr> can cause sections to disappear or nest incorrectly.
- Make sure all the sections you added are inside the <div class="quotation"> container, not outside it.
- Use the double-check tab below to compare your code against the full file.
Still stuck? Help me figure out why my quotation HTML page is not rendering all sections correctly.
✔️ Awesome, I've got everything!
Great. Double check you have saved index.html.
ⓧ I'd like to double check the full code
Your index.html file is longer than 30 lines, so scroll through your file and confirm each section is present in this order:
- The <!DOCTYPE html> declaration, <head> with the stylesheet link, and <body> with the <div class="quotation"> wrapper.
- The quotation-header with company-info (h1 and four p tags, all contenteditable) and quotation-title (h2 saying QUOTATION).
- The quotation-meta div containing meta-left (5 client input rows) and meta-right (4 quote detail input rows).
- The items-table with a thead (9 column headers) and a tbody containing one data row with inputs and a Remove button.
- A controls no-print div with the "+ Add Item" button.
- The totals div with Subtotal, VAT (5%), and Grand Total rows.
- The terms div with a contenteditable block of four terms.
- A final controls no-print div with the "Print / Save as PDF" button.
- The <script src="script.js"> tag just before </body>.
Your quotation has all its content in place. Next up, you will transform this messy wall of text into a professional-looking business document with CSS styling.
Style Your Quotation
Your quotation's structure is complete. Every section is in place, from the company header down to the terms and conditions.
Right now, though, it looks like an unformatted wall of text and input boxes. CSS will transform it into a professional business document with clean typography, a branded color scheme, and an organized layout.
In this step, get ready to:
- Add base styles that create a clean white document on a gray background.
- Style the header and metadata sections with a branded color scheme and side-by-side layout.
- Style the table, totals, terms, and buttons to complete the professional look.
Add base styles to your document
Every browser applies its own default margins and padding to HTML elements. A CSS reset removes those defaults so your design starts from a clean slate.
- In VS Code, click on style.css in the Explorer sidebar to open it.
- Add the CSS reset and base layout styles by pasting this code:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
padding: 20px;
color: #333;
}
.quotation {
max-width: 900px;
margin: 0 auto;
background-color: #fff;
padding: 40px;
border: 1px solid #ddd;
}
What does this code do?
- The * selector targets every element on the page, stripping away default margins, padding, and switching to border-box sizing so padding never pushes elements wider than you set them.
- The body gets a light gray background (#f0f0f0) so the white quotation container pops off the page.
- The .quotation container is capped at 900px wide and centered with margin: 0 auto, creating a crisp paper-like document in the middle of the screen.
- Save style.css by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Switch to your browser and refresh the page showing index.html.
- You should see a white rectangle centered on a light gray background. The text inside is now tighter, with no extra gaps around the edges.
Still seeing a plain white page?
- Make sure you saved style.css. Unsaved changes won't appear in the browser.
- Confirm that your index.html file contains <link rel="stylesheet" href="style.css"> inside the <head> tag. This is what connects your CSS to the HTML.
- Check that style.css is in the same folder as index.html. If it's in a subfolder, the link won't find it.
Why isn't my CSS applying to my HTML page?
Style the header and metadata sections
The company header needs to look like a professional letterhead, with the company info on the left and the "QUOTATION" title on the right. The client details and quote metadata below it need to sit side by side.
- Add the header styles below the existing code in style.css:
.quotation-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 3px solid #2c3e50;
padding-bottom: 20px;
margin-bottom: 20px;
}
.company-info h1 {
font-size: 24px;
color: #2c3e50;
margin-bottom: 5px;
}
.company-info p {
font-size: 13px;
color: #555;
line-height: 1.6;
}
.quotation-title h2 {
font-size: 28px;
color: #2c3e50;
letter-spacing: 2px;
}
What does this code do?
- display: flex on the header puts the company info and the QUOTATION title on the same row. justify-content: space-between pushes them to opposite sides.
- The dark bottom border (#2c3e50) creates a clean divider between the header and the rest of the document. This color is used throughout as the brand color.
- Add the metadata styles below the header styles in style.css:
.quotation-meta {
display: flex;
justify-content: space-between;
margin-bottom: 25px;
gap: 20px;
}
.meta-left,
.meta-right {
flex: 1;
}
.meta-table {
width: 100%;
border-collapse: collapse;
}
.meta-table td {
padding: 4px 0;
font-size: 13px;
}
.meta-label {
font-weight: bold;
width: 110px;
color: #2c3e50;
}
.meta-table input {
width: 100%;
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 13px;
font-family: Arial, sans-serif;
}
What does this code do?
- flex: 1 on both .meta-left and .meta-right makes them share the available width equally. The client details sit on the left, and the quote metadata sits on the right.
- border-collapse: collapse removes the double-border effect that tables have by default, giving you clean single lines.
- The input fields get light borders and rounded corners (border-radius: 3px) for a modern, polished feel.
- Save style.css and refresh your browser.
- You should see a dark horizontal line separating the header from the rest of the page. The company name is larger and dark blue. The client details and quote metadata fields now sit side by side with neat labels and bordered input fields.
Client details and quote details stacking vertically?
- Check that your .quotation-meta rule includes display: flex. Without it, the two sections stack on top of each other instead of sitting side by side.
- Make sure the class name .quotation-meta in your CSS matches class="quotation-meta" in your HTML exactly. CSS class names are case-sensitive.
Why are my CSS flex layouts not working on my quotation page?
Style the table, totals, terms, and buttons
The line items table is the core of the quotation. It needs a dark header row so the column names stand out, consistent cell spacing, and styled inputs that blend into the table rather than looking like floating text boxes.
- Add the table structure styles below the existing code in style.css:
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
}
.items-table thead {
background-color: #2c3e50;
color: #fff;
}
.items-table th {
padding: 10px 8px;
font-size: 13px;
text-align: left;
font-weight: bold;
}
.items-table td {
padding: 6px 8px;
border-bottom: 1px solid #eee;
font-size: 13px;
}
.items-table tbody tr:hover {
background-color: #f9f9f9;
}
.items-table input {
width: 100%;
padding: 4px 6px;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 13px;
font-family: Arial, sans-serif;
}
What does this code do?
- The thead gets the same dark blue background (#2c3e50) with white text, making the column headers stand out clearly.
- The tr:hover rule adds a subtle gray highlight when the mouse hovers over a row, helping the user track which line they are reading.
- Add the column-specific widths and row styling below the table styles:
.desc-input {
min-width: 150px;
}
.small-input {
max-width: 60px;
}
.qty-input,
.price-input {
max-width: 80px;
text-align: right;
}
.amount {
text-align: right;
font-weight: bold;
min-width: 80px;
}
.row-number {
text-align: center;
font-weight: bold;
color: #777;
}
.remove-btn {
background-color: #e74c3c;
color: #fff;
border: none;
padding: 4px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
}
.remove-btn:hover {
background-color: #c0392b;
}
What does this code do?
- The max-width and min-width values give each column a sensible size. Numeric columns like QTY and Unit Price are narrow and right-aligned, while Description gets more room.
- The .remove-btn is styled red (#e74c3c) to signal a destructive action. The :hover darkens the color so the button feels responsive when the user moves their mouse over it.
- Save style.css and refresh your browser.
- The table now has a dark header row with white text, neatly sized columns, and a red Remove button. Hover your mouse over a table row to see the subtle gray highlight.
- Add the button and controls styles below the existing code in style.css:
.controls {
margin: 15px 0;
text-align: center;
}
#addRowBtn {
background-color: #27ae60;
color: #fff;
border: none;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
#addRowBtn:hover {
background-color: #219a52;
}
#printBtn {
background-color: #2c3e50;
color: #fff;
border: none;
padding: 10px 30px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
#printBtn:hover {
background-color: #1a252f;
}
What does this code do?
- Each button gets a distinct color to signal its purpose. Green (#27ae60) for Add Item means "go ahead, add something." The dark blue (#2c3e50) Print button matches the header, keeping it professional.
- cursor: pointer changes the mouse cursor to a hand icon when hovering over a button, telling the user it is clickable.
- Save style.css and refresh your browser.
- The "+ Add Item" button is now green and centered below the table. The "Print / Save as PDF" button at the bottom is dark blue. Hover over each button to see the color darken.
- Add the totals section styles below the existing code in style.css:
.totals {
display: flex;
justify-content: flex-end;
margin-bottom: 25px;
}
.totals-table {
border-collapse: collapse;
min-width: 250px;
}
.totals-table td {
padding: 6px 12px;
font-size: 14px;
}
.totals-label {
text-align: right;
font-weight: bold;
color: #2c3e50;
}
.totals-value {
text-align: right;
min-width: 100px;
}
.grand-total-row {
border-top: 2px solid #2c3e50;
}
.grand-total-row td {
font-size: 16px;
padding-top: 10px;
font-weight: bold;
color: #2c3e50;
}
What does this code do?
- justify-content: flex-end pushes the totals table to the right side of the page, exactly where you would expect to see totals on a real invoice.
- The .grand-total-row gets a top border and larger, bolder text so the final number jumps out at a glance.
- Add the terms section styles as the last block of code in style.css:
.terms {
border-top: 1px solid #ddd;
padding-top: 15px;
margin-bottom: 20px;
}
.terms h3 {
font-size: 14px;
color: #2c3e50;
margin-bottom: 8px;
}
.terms p {
font-size: 12px;
color: #666;
line-height: 1.8;
}
What does this code do?
The terms section gets a subtle top border to separate it from the totals above. The text is deliberately smaller (12px) and lighter (#666) than the main content, keeping the fine print from competing with the pricing details.
Before you refresh, what do you think the full quotation will look like now that all the CSS is in place?
- Save style.css and refresh your browser.
The quotation now looks like a professional business document. You should see a white page centered on a gray background, a dark header bar with your company name, neatly aligned client and metadata fields side by side, a styled table with a dark header row, color-coded buttons (green for Add Item, red for Remove, dark for Print), right-aligned totals with a bold Grand Total, and smaller terms text at the bottom.
Quotation not looking right?
- If a specific section looks unstyled, check that the class name in your CSS matches the class name in your HTML. A single typo (like .quotation-headr instead of .quotation-header) will silently skip that rule.
- Make sure every CSS rule has matching opening { and closing } braces. A missing brace can break all the styles that come after it.
- Try a hard refresh by pressing Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows). This clears the browser's cached version of your CSS file.
Help me figure out why part of my CSS styling is not showing up.
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css and your browser shows the fully styled quotation.
ⓧ I'd like to double check the full code
Scroll up through your work and confirm you have all of these CSS blocks in style.css, in this order:
- CSS reset (*), body, and .quotation container styles.
- .quotation-header, .company-info, and .quotation-title styles.
- .quotation-meta, .meta-left/.meta-right, .meta-table, .meta-label, and .meta-table input styles.
- .items-table structure, thead, th, td, hover, and input styles.
- .desc-input, .small-input, .qty-input/.price-input, .amount, .row-number, and .remove-btn styles.
- .controls, #addRowBtn, and #printBtn styles.
- .totals, .totals-table, .totals-label, .totals-value, and .grand-total-row styles.
- .terms, .terms h3, and .terms p styles.
Your quotation looks the part now. Next up, you will bring it to life with JavaScript so you can add line items, calculate totals, and make the form truly interactive.
Add Dynamic Line Items and Calculations
Your quotation looks like a real business document now. But try clicking the + Add Item button or typing a quantity and price into the first row. Nothing happens.
The HTML gives us structure and CSS gives us style, but neither can respond to what a user does. JavaScript is the language that makes a web page interactive. In this step, you will write the JavaScript that adds rows, removes rows, and calculates every total in real time.
In this step, get ready to:
- Write a calculation function that computes line amounts, subtotal, VAT, and grand total.
- Build functions to dynamically add and remove table rows.
- Wire up event listeners so totals update instantly as you type.
Write the calculation and listener functions
The quotation needs a brain. When someone types a quantity and a unit price, the tool should multiply them to get the line amount, add up all the line amounts into a subtotal, compute 5% VAT, and display the grand total.
- Switch back to VS Code and open script.js in the editor.
- Add the calculateTotals() function by pasting this code:
function calculateTotals() {
// Get all rows from the line items table
var tbody = document.getElementById('itemsBody');
var rows = tbody.querySelectorAll('tr');
var subtotal = 0;
// Loop through each row to calculate the line amount
for (var i = 0; i < rows.length; i++) {
var qty = parseFloat(rows[i].querySelector('.qty-input').value) || 0;
var price = parseFloat(rows[i].querySelector('.price-input').value) || 0;
var amount = qty * price;
// Display the line amount and add to running subtotal
rows[i].querySelector('.amount').textContent = amount.toFixed(2);
subtotal += amount;
}
// Calculate VAT at 5% and the grand total
var vat = subtotal * 0.05;
var grandTotal = subtotal + vat;
// Update the totals section on the page
document.getElementById('subtotal').textContent = subtotal.toFixed(2);
document.getElementById('vat').textContent = vat.toFixed(2);
document.getElementById('grandTotal').textContent = grandTotal.toFixed(2);
}
What does this code do?
This function is the math engine of the entire quotation. Every time it runs, it loops through every row in the table and reads the QTY and Unit Price values.
- parseFloat() converts the text from each input field into a number. The || 0 part means "if the input is empty or not a number, use 0 instead."
- toFixed(2) formats every number to exactly two decimal places, so you see 500.00 instead of 500.
- After looping through all rows, it calculates VAT as 5% of the subtotal and adds both together for the grand total.
The calculateTotals() function exists now, but nothing calls it yet. You need two more pieces: a function that listens for changes in QTY and Unit Price fields, and a startup block that wires up the first row when the page loads.
- Below your calculateTotals() function, add the attachInputListeners() function and the page startup listener by pasting this code:
function attachInputListeners(row) {
// Find the quantity and price inputs in this row
var qtyInput = row.querySelector('.qty-input');
var priceInput = row.querySelector('.price-input');
// Recalculate all totals whenever either field changes
qtyInput.addEventListener('input', calculateTotals);
priceInput.addEventListener('input', calculateTotals);
}
// Run setup once the page finishes loading
document.addEventListener('DOMContentLoaded', function() {
// Attach listeners to the starter row from the HTML
var firstRow = document.querySelector('#itemsBody tr');
attachInputListeners(firstRow);
// Set today's date as the default Created Date
var today = new Date().toISOString().split('T')[0];
document.getElementById('createdDate').value = today;
});
What does this code do?
- attachInputListeners() takes a table row and attaches input event listeners to its QTY and Unit Price fields. Every keystroke in either field triggers calculateTotals().
- The DOMContentLoaded event fires once the browser has finished reading all the HTML. Inside it, the code wires up the first row that already exists in your HTML and sets today's date in the Created Date field.
- Save script.js.
- Switch to your browser and refresh the page.
- Notice the Created Date field now shows today's date automatically.
- In the first row, type 3 in the QTY field and 50 in the Unit Price field.
The Amount column should instantly show 150.00. The Subtotal reads 150.00, VAT reads 7.50, and Grand Total reads 157.50.
Totals not updating when you type?
- Make sure you saved script.js and refreshed the browser. Unsaved changes are not picked up.
- Check that the attachInputListeners function is placed above the DOMContentLoaded block, not inside it.
- Open your browser's developer console (press F12, then click the Console tab) and look for red error messages. They often point to the exact line with the typo.
Why aren't my totals updating when I type in the QTY or Unit Price fields?
Add new line items dynamically
The first row works, but a real quotation usually has multiple items. The green + Add Item button in your HTML already calls a function named addRow(). That function does not exist yet, so nothing happens when you click it.
- In script.js, place your cursor right above the calculateTotals() function. Add the addRow() and updateRowNumbers() functions by pasting this code:
function addRow() {
var tbody = document.getElementById('itemsBody');
var rowCount = tbody.rows.length + 1;
// Create a new row matching the starter row's structure
var row = document.createElement('tr');
row.innerHTML =
'<td class="row-number">' + rowCount + '</td>' +
'<td><input type="text" placeholder="Brand"></td>' +
'<td><input type="text" placeholder="Item ID"></td>' +
'<td><input type="text" placeholder="Description" class="desc-input"></td>' +
'<td><input type="text" placeholder="UOM" class="small-input"></td>' +
'<td><input type="number" class="qty-input" value="0" min="0"></td>' +
'<td><input type="number" class="price-input" value="0" min="0" step="0.01"></td>' +
'<td class="amount">0.00</td>' +
'<td class="no-print"><button class="remove-btn" onclick="removeRow(this)">Remove</button></td>';
tbody.appendChild(row);
attachInputListeners(row);
updateRowNumbers();
}
function updateRowNumbers() {
// Renumber all rows sequentially
var tbody = document.getElementById('itemsBody');
var rows = tbody.querySelectorAll('tr');
for (var i = 0; i < rows.length; i++) {
rows[i].querySelector('.row-number').textContent = i + 1;
}
}
What does this code do?
- addRow() builds a brand-new table row using document.createElement('tr') and fills it with the same input fields as the starter row in your HTML. It then appends it to the table, hooks up its QTY and Price listeners, and renumbers all rows.
- updateRowNumbers() loops through every row and sets its Sr # cell to the correct position. This keeps numbering clean when rows are added or removed from the middle.
- Save script.js and refresh your browser.
- Click the + Add Item button.
A second row appears in the table with row number 2. Click + Add Item a few more times and watch the row numbers increment. Try typing a quantity and price in one of the new rows and confirm the totals update instantly.
New rows not appearing?
- Open the browser console (F12 then Console tab). If you see a red error mentioning addRow is not defined, make sure you saved the file after pasting the function.
- Check that the addRow() function is outside of any other function. It should be at the top level of script.js, not nested inside DOMContentLoaded.
Why doesn't clicking Add Item create a new row in my quotation table?
Remove line items
You can add rows, but there is no way to remove one if you add too many or make a mistake. Each row's red Remove button already calls removeRow(this) in the HTML. You just need the function.
- In script.js, add the removeRow() function directly below addRow() by pasting this code:
function removeRow(button) {
var tbody = document.getElementById('itemsBody');
// Only remove if more than one row remains
if (tbody.rows.length > 1) {
button.closest('tr').remove();
updateRowNumbers();
calculateTotals();
}
}
What does this code do?
- button.closest('tr') walks up the DOM tree from the clicked Remove button until it finds the parent <tr> element, then removes that entire row.
- The if (tbody.rows.length > 1) check prevents the learner from deleting the very last row. A quotation always needs at least one line item.
- After removing the row, it renumbers the remaining rows and recalculates all totals so the numbers stay correct.
- Save script.js and refresh your browser.
Before you test, think about this: if you type a quantity of 5 and a unit price of 100 in a row, what do you expect the Subtotal, VAT, and Grand Total to show?
- Type 5 in the QTY field and 100 in the Unit Price field of the first row.
The Amount column shows 500.00. Subtotal reads 500.00, VAT reads 25.00, and Grand Total reads 525.00.
- Click + Add Item to add a second row.
- Type 2 in the new row's QTY and 50 in Unit Price. The Grand Total updates to 630.00 (500 + 100 = 600 subtotal, plus 30 VAT).
- Click Remove on the second row. The totals drop back to 525.00.
Remove button not working?
- If the row disappears but totals do not update, check that removeRow() calls both updateRowNumbers() and calculateTotals() inside the if block.
- If nothing happens at all, open the browser console (F12) and look for a red error. A typo in closest is a common cause.
Clicking Remove does not delete the row or update the totals. What should I check?
✔️ Awesome, I've got everything!
Great. Double check you have saved script.js and that all five functions plus the DOMContentLoaded block are present.
ⓧ I'd like to double check the full code
Your script.js should contain these five functions and one startup block, in any order. Scroll through your file and confirm each one is present:
- addRow() creates a new table row with all input fields and appends it to the table.
- removeRow(button) removes the row only if more than one exists, then renumbers and recalculates.
- updateRowNumbers() loops through all rows and sets each Sr # to its correct position.
- calculateTotals() reads QTY and Unit Price from every row, computes line amounts, subtotal, VAT (5%), and grand total.
- attachInputListeners(row) adds input event listeners on QTY and Unit Price to trigger calculateTotals().
- DOMContentLoaded listener that attaches listeners to the first row and sets today's date in #createdDate.
Your quotation tool is fully interactive. You can add items, remove them, and every total recalculates in real time. Next up, you will make it print-ready so you can save a clean, professional PDF.
Print Your Quotation as PDF
Your quotation tool is fully functional. You can add items, fill in client details, and watch totals calculate in real time. The next question is how to turn this into a document you can actually send to a client.
The Print / Save as PDF button already calls window.print() to open your browser's print dialog. But right now, the print preview shows every editing control, making the output look like a web form instead of a professional business document.
In this step, get ready to:
- See what the print preview looks like before any print-specific styling.
- Add @media print CSS rules to hide editing controls and produce a clean PDF.
See what happens when you print
Before you click the button, what do you think the print preview will look like? Will it show a clean document, or will you see every button and input border from the editing view?
- Switch to your browser where the quotation is open.
- Click the Print / Save as PDF button at the bottom of the page.
The print preview is a mess. The green + Add Item button, the red Remove buttons, and the Print / Save as PDF button all appear in the document.
Every input field has a visible border, and the gray page background is showing. This looks like a web form screenshot, not a quotation you would send to a client.
Why does this happen?
By default, browsers print exactly what they display on screen. They do not know which elements are editing controls and which are part of the final document.
This is the exact gap you will fix. CSS has a feature called @media print that lets you write styles that only apply when the page is printed or saved as a PDF.
- Close the print preview by clicking Cancel or pressing the Escape key.
Add print-friendly CSS rules
You need rules that only kick in when the browser is printing. The @media print block is the standard way to do this. Any CSS you write inside it is completely ignored on screen and only applies during printing.
- Switch back to VS Code and open style.css.
- Scroll to the very bottom of the file, after the .terms p rule.
- Add the following @media print block to hide buttons and fix the page layout for printing:
@media print {
body {
background-color: #fff;
padding: 0;
}
.quotation {
border: none;
padding: 20px;
}
.no-print {
display: none;
}
}
What does this code do?
- @media print { } wraps all the rules so they only apply when printing. On screen, these rules are invisible.
- The body rule switches the gray background to white and removes the padding, so the document fills the page.
- .quotation loses its border so it does not look like a box floating on a page.
- .no-print hides every element with that class. If you check your HTML, the + Add Item button, Remove buttons, and Print / Save as PDF button all carry the no-print class.
- Save style.css.
- Switch to your browser, refresh the page, and click Print / Save as PDF.
The buttons are gone and the background is white. But the input fields still have visible borders, making every field look like an empty text box instead of clean text.
- Close the print preview.
Still seeing the buttons in the preview?
- Make sure you saved style.css before refreshing the browser.
- Check that your @media print block is outside all other CSS rules, at the very bottom of the file. Nesting it inside another rule will break it.
- Confirm the class name matches exactly: .no-print in your CSS and no-print in your HTML. CSS class names are case-sensitive.
Why are my buttons still showing in the print preview?
Now you need to clean up the remaining visual artifacts. Input borders, hover highlights, and contenteditable outlines all need to be removed so the printed output looks like a finished document.
- In style.css, find the closing } of your @media print block (the last line of the file).
- Add the following rules just above that closing } to strip input borders and contenteditable outlines:
.meta-table input,
.items-table input {
border: none;
padding: 0;
}
.items-table tbody tr:hover {
background-color: transparent;
}
.terms div {
border: none;
outline: none;
}
.company-info h1,
.company-info p {
border: none;
outline: none;
}
What do these rules clean up?
- .meta-table input and .items-table input remove the border and padding from every input field, so the values look like plain text in the printed document.
- .items-table tbody tr:hover sets the hover background to transparent. Without this, some browsers render the hover highlight in the print output.
- The .terms div and .company-info rules remove any outlines that browsers add around contenteditable elements when printing.
- Save style.css.
Before you click Print / Save as PDF again, what do you expect to see this time? Think about which problems from your first preview should now be fixed.
- Refresh the page in your browser.
- Click Print / Save as PDF.
The print preview now shows a clean, professional quotation. No buttons, no input borders, no gray background. The company header, client details, line items, totals, and terms all display as plain text on a white page.
- Select Save as PDF as the destination in the print dialog to export your quotation as a PDF file.
Still seeing input borders in the print preview?
- Check that your new rules are inside the @media print { } block, not after its closing brace. Rules outside the block apply to the screen view, not the print view.
- Make sure you typed border: none; not border: 0;. Both work in most cases, but match the code exactly to avoid any differences.
- Try a hard refresh with Ctrl+Shift+R to clear any cached CSS.
Why are input borders still appearing in my print preview?
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css.
ⓧ I'd like to double check the full code
The full style.css file is long, so scroll to the very bottom and confirm your @media print block contains these rule sets in order:
- body with white background and no padding.
- .quotation with no border and 20px padding.
- .no-print with display: none.
- .meta-table input and .items-table input with no border and no padding.
- .items-table tbody tr:hover with transparent background.
- .terms div with no border and no outline.
- .company-info h1 and .company-info p with no border and no outline.
All seven selectors should be inside one @media print { } block at the bottom of your file.
Your quotation tool is complete. You can fill in company details, add line items with auto-calculated totals, and export a polished PDF with a single click. Next up, there is an optional Secret Mission to push your skills even further.
Secret mission
Save and Load Quotation Drafts
Your quotation tool works perfectly, but every time you refresh the page, all your data disappears. In this mission, you will use the browser's localStorage API to save quotation drafts and reload them later, so your work survives a page refresh.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files, or delete them entirely.
Resources you used:
- The quotation-tool folder on your Desktop containing index.html, style.css, and script.js.
- Browser localStorage data stored under the savedQuote key (if you completed the Secret Mission).
Keep everything running
No action needed. Choose this if you want to keep editing your quotation tool or use it for real quotations.
- Your quotation-tool folder stays on your Desktop and you can open index.html in your browser any time to create new quotations.
- Any saved drafts in localStorage will persist until you clear your browser data.
- Visual Studio Code remains installed on your machine for future projects.
Pause - I'll come back to this later
Since this project is just files on your Desktop, there is nothing to pause. Your files will be right where you left them whenever you come back.
- Close the browser tab showing your quotation.
- Close Visual Studio Code if you have it open.
- When you are ready to return, open the quotation-tool folder in VS Code and double-click index.html to reopen it in your browser.
Delete - I don't want to use this again
Remove all project files and stored data to start fresh.
- Delete the project folder. On macOS or Linux, open a terminal and run the command below. On Windows, open PowerShell and run the second command.
macOS
rm -rf ~/Desktop/quotation-tool
Windows
rmdir /s /q "%USERPROFILE%\Desktop\quotation-tool"
- Clear the saved quotation draft from your browser. Open Chrome or Edge, press F12 to open DevTools, click the Console tab, and run this command:
localStorage.removeItem('savedQuote')
This removes the savedQuote key that the Save Draft feature stored in your browser.
Nice Work!
Nice Work!
Nice work! You just built a professional client price quotation tool using nothing but HTML, CSS, and JavaScript.
You've learned how to:
- Built a browser-based quotation form with editable company details, client information, quotation metadata, and a dynamic line items table that grows and shrinks on demand.
- Added real-time calculations that instantly update line amounts, subtotal, VAT at 5%, and grand total as you type quantities and prices.
- Created a print-ready PDF export using @media print CSS rules that hide all editing controls and produce a clean, professional document.
- Secret Mission: Completed an optional challenge to extend your quotation tool with additional functionality.
Ready to quiz yourself?