Build an Invoice Generator 📄
Generating PDFs often feels daunting (requiring heavy libraries like react-pdf or backend services).
But for invoices, there's a simpler trick: Print Styles.
You can generate a clean invoice using pure HTML/CSS and use the browser's native Print to PDF functionality.
Step 1: Dynamic Line Items 📝
Invoices need an array of items that users can add, remove, or edit.
Use a unique id (like Date.now()) for each row to handle React rendering correctly.
const [items, setItems] = useState([
{ id: 1, desc: "Web Design", qty: 10, rate: 50 },
{ id: 2, desc: "Hosting", qty: 1, rate: 100 }
]);
// Add Row
const addItem = () => {
setItems([...items, { id: Date.now(), desc: "", qty: 1, rate: 0 }]);
};
// Calculate Subtotal
const subtotal = items.reduce((sum, item) => sum + (item.qty * item.rate), 0);
Step 2: The "Print Window" Trick 🖨️
Instead of complex PDF libraries, use a popup window.
- Open a new window:
window.open('', '_blank') - Write your Invoice HTML into it.
- Call
window.print().
This triggers the browser's print dialog, which has a built-in "Save as PDF" option that renders 100% accurate vector text.
const printInvoice = () => {
const win = window.open('', '_blank');
win.document.write(`
<html>
<head>
<title>Invoice #1001</title>
<style>
body { font-family: sans-serif; padding: 40px; }
.header { display: flex; justify-content: space-between; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border-bottom: 1px solid #ddd; padding: 10px; text-align: left; }
</style>
</head>
<body>
<div class="header">
<h1>INVOICE</h1>
<div>Date: ${new Date().toLocaleDateString()}</div>
</div>
<table>
<thead>
<tr><th>Item</th><th>Qty</th><th>Price</th><th>Total</th></tr>
</thead>
<tbody>
${items.map(item => `
<tr>
<td>${item.desc}</td>
<td>${item.qty}</td>
<td>$${item.rate}</td>
<td>$${item.qty * item.rate}</td>
</tr>
`).join('')}
</tbody>
</table>
</body>
</html>
`);
win.document.close();
win.print();
};
Step 3: The React Component 💼
A clean editor where you can edit fields live, then "Print" to save.
"use client"
import { useState } from "react"
import { Printer, Plus, Trash2 } from "lucide-react"
export default function InvoiceGen() {
const [items, setItems] = useState([{ id: 1, desc: "Consulting", qty: 5, rate: 100 }])
const subtotal = items.reduce((s, i) => s + i.qty * i.rate, 0);
const total = subtotal * 1.05; // 5% Tax example
return (
<div className="max-w-2xl mx-auto p-8 bg-white shadow-xl border border-slate-200">
<div className="flex justify-between items-center mb-8">
<h1 className="text-4xl font-black text-slate-800 tracking-tighter">INVOICE</h1>
<button onClick={() => window.print()} className="flex items-center gap-2 bg-slate-800 text-white px-4 py-2 rounded-lg hover:bg-slate-900 print:hidden">
<Printer size={16} /> Print / Save PDF
</button>
</div>
{/* Line Items */}
<div className="space-y-2 mb-8">
{items.map((item, idx) => (
<div key={item.id} className="flex gap-2 items-center">
<input
className="flex-1 p-2 bg-slate-50 border border-slate-200 rounded"
value={item.desc}
onChange={e => {
const newItems = [...items];
newItems[idx].desc = e.target.value;
setItems(newItems);
}}
/>
<input className="w-16 p-2 bg-slate-50 border border-slate-200 rounded text-center" value={item.qty} type="number" readOnly />
<input className="w-24 p-2 bg-slate-50 border border-slate-200 rounded text-right" value={item.rate} type="number" readOnly />
<div className="w-24 text-right font-bold text-slate-700">
${(item.qty * item.rate).toFixed(2)}
</div>
</div>
))}
</div>
<div className="flex justify-end border-t border-slate-200 pt-4">
<div className="w-48 space-y-2">
<div className="flex justify-between text-slate-500">
<span>Subtotal</span>
<span>${subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between font-black text-xl text-slate-800">
<span>Total</span>
<span>${total.toFixed(2)}</span>
</div>
</div>
</div>
</div>
)
}
Pro Tip: CSS for Printing 🖌️
Use @media print to hide buttons and UI elements (like the "Print" button itself) when the user actually prints.
@media print {
.print\:hidden {
display: none !important;
}
}