DEV Community

Ivan
Ivan

Posted on

Generate PDF & Excel Files Directly in Browser with Quasar — No Backend Needed!

By Ivan Rocha Cardoso - Full-Stack Developer at SiGlobal

Have you ever needed to generate a PDF report or Excel spreadsheet directly from your app without depending on a backend? Today I'll show you how to do this using only Vue.js + Quasar.

Works 100% in the browser, exports tables, reports, and dashboards in a simple and professional way.


Introduction: Why generate PDF/Excel on the frontend?

Common use cases:

  • Sales reports that need to be printed or emailed
  • Dashboards with data that users want to save locally
  • Data tables that need to be exported for analysis
  • Offline applications that can't depend on server to generate files
  • Cost reduction of infrastructure (no need for server-side libraries)

Frontend approach advantages:

Performance: Distributed processing on users' devices

Simplicity: No additional APIs to maintain

Offline: Works even without internet connection

Cost: Reduces server load


⚙️ Setup: Starting the project

1. Creating the Quasar project

# Install Quasar CLI (if you don't have it yet) npm install -g @quasar/cli # Create new project quasar create pdf-excel-project # or npm init quasar@latest # Navigate to directory cd pdf-excel-project 
Enter fullscreen mode Exit fullscreen mode

2. Installing required dependencies

# For PDF generation npm install pdfmake # For Excel generation npm install xlsx # Auxiliary dependencies for types (optional) npm install --save-dev @types/pdfmake 
Enter fullscreen mode Exit fullscreen mode

3. Project structure

src/ ├── components/ │ ├── ExportPDF.vue │ └── ExportExcel.vue ├── pages/ │ └── ExportDemo.vue └── utils/ ├── pdfGenerator.js └── excelGenerator.js 
Enter fullscreen mode Exit fullscreen mode

Practical Example 1: Export QTable to Excel

ExportExcel.vue Component

<template> <div class="q-pa-md"> <q-table title="Monthly Sales" :rows="salesData" :columns="columns" row-key="id" class="q-mb-md" /> <q-btn color="green" icon="download" label="Export Excel" @click="exportToExcel" /> </div> </template> <script> import * as XLSX from 'xlsx' export default { name: 'ExportExcel', data() { return { columns: [ { name: 'id', label: 'ID', field: 'id', align: 'left' }, { name: 'product', label: 'Product', field: 'product', align: 'left' }, { name: 'salesperson', label: 'Salesperson', field: 'salesperson', align: 'left' }, { name: 'amount', label: 'Amount', field: 'amount', format: val => `$${val}` }, { name: 'date', label: 'Date', field: 'date' } ], salesData: [ { id: 1, product: 'Dell Laptop', salesperson: 'John Smith', amount: 2500.00, date: '2024-01-15' }, { id: 2, product: 'Logitech Mouse', salesperson: 'Mary Johnson', amount: 85.50, date: '2024-01-16' }, { id: 3, product: 'Mechanical Keyboard', salesperson: 'Peter Wilson', amount: 350.00, date: '2024-01-17' }, { id: 4, product: '24" Monitor', salesperson: 'Anna Davis', amount: 800.00, date: '2024-01-18' } ] } }, methods: { exportToExcel() { // Prepare data for Excel const worksheetData = [ // Header ['ID', 'Product', 'Salesperson', 'Amount', 'Date'], // Data ...this.salesData.map(row => [ row.id, row.product, row.salesperson, row.amount, row.date ]) ] // Create worksheet const worksheet = XLSX.utils.aoa_to_sheet(worksheetData) // Create workbook const workbook = XLSX.utils.book_new() XLSX.utils.book_append_sheet(workbook, worksheet, 'Sales') // Export file XLSX.writeFile(workbook, `sales_${new Date().toISOString().split('T')[0]}.xlsx`) this.$q.notify({ color: 'green-4', textColor: 'white', icon: 'cloud_download', message: 'Excel exported successfully!' }) } } } </script> 
Enter fullscreen mode Exit fullscreen mode

📄 Practical Example 2: Generate PDF with Logo and Formatting

ExportPDF.vue Component

<template> <div class="q-pa-md"> <q-card class="q-mb-md"> <q-card-section> <div class="text-h6">Sales Report</div> <div class="text-subtitle2">Data visualization</div> </q-card-section> <q-card-section> <q-table :rows="salesData" :columns="columns" row-key="id" hide-pagination :rows-per-page-options="[0]" /> </q-card-section> </q-card> <q-btn color="red" icon="picture_as_pdf" label="Generate PDF" @click="generatePDF" /> </div> </template> <script> import pdfMake from 'pdfmake/build/pdfmake' import pdfFonts from 'pdfmake/build/vfs_fonts' // Configure fonts pdfMake.vfs = pdfFonts export default { name: 'ExportPDF', data() { return { columns: [ { name: 'id', label: 'ID', field: 'id', align: 'left' }, { name: 'product', label: 'Product', field: 'product', align: 'left' }, { name: 'salesperson', label: 'Salesperson', field: 'salesperson', align: 'left' }, { name: 'amount', label: 'Amount', field: 'amount', format: val => `$${val}` }, { name: 'date', label: 'Date', field: 'date' } ], salesData: [ { id: 1, product: 'Dell Laptop', salesperson: 'John Smith', amount: 2500.00, date: '2025-01-15' }, { id: 2, product: 'Logitech Mouse', salesperson: 'Mary Johnson', amount: 85.50, date: '2025-01-16' }, { id: 3, product: 'Mechanical Keyboard', salesperson: 'Peter Wilson', amount: 350.00, date: '2025-01-17' }, { id: 4, product: '24" Monitor', salesperson: 'Anna Davis', amount: 800.00, date: '2025-01-18' } ] } }, methods: { generatePDF() { // Prepare table data for PDF const tableBody = [ // Table header [ { text: 'ID', style: 'tableHeader' }, { text: 'Product', style: 'tableHeader' }, { text: 'Salesperson', style: 'tableHeader' }, { text: 'Amount', style: 'tableHeader' }, { text: 'Date', style: 'tableHeader' } ], // Table data ...this.salesData.map(row => [ row.id.toString(), row.product, row.salesperson, `$${row.amount.toFixed(2)}`, row.date ]) ] // Calculate total const total = this.salesData.reduce((sum, item) => sum + item.amount, 0) // Define PDF structure const docDefinition = { content: [ // Header { columns: [ { text: 'SALES REPORT', style: 'header' }, { text: `Date: ${new Date().toLocaleDateString('en-US')}`, style: 'subheader', alignment: 'right' } ] }, // Divider line { text: '', margin: [0, 10, 0, 10] }, // Table { table: { headerRows: 1, widths: ['auto', '*', '*', 'auto', 'auto'], body: tableBody }, layout: { fillColor: function (rowIndex) { return (rowIndex % 2 === 0) ? '#f3f3f3' : null } } }, // Total { text: `Grand Total: $${total.toFixed(2)}`, style: 'total', margin: [0, 20, 0, 0] }, // Footer { text: 'Report automatically generated by the system', style: 'footer', margin: [0, 30, 0, 0] } ], // Styles styles: { header: { fontSize: 18, bold: true, color: '#2c3e50' }, subheader: { fontSize: 12, color: '#7f8c8d' }, tableHeader: { bold: true, fontSize: 12, color: 'white', fillColor: '#3498db' }, total: { fontSize: 14, bold: true, alignment: 'right', color: '#27ae60' }, footer: { fontSize: 10, italics: true, alignment: 'center', color: '#95a5a6' } } } // Generate and download PDF pdfMake.createPdf(docDefinition).download(`sales_report_${new Date().toISOString().split('T')[0]}.pdf`) this.$q.notify({ color: 'red-4', textColor: 'white', icon: 'picture_as_pdf', message: 'PDF generated successfully!' }) } } } </script> 
Enter fullscreen mode Exit fullscreen mode

Advanced Improvements

1. Excel with custom formatting

// utils/excelGenerator.js import * as XLSX from 'xlsx' export function exportAdvancedExcel(data, filename) { const worksheet = XLSX.utils.aoa_to_sheet(data) // Apply styles (limited in XLSX) const range = XLSX.utils.decode_range(worksheet['!ref']) // Cell formatting for (let row = range.s.r; row <= range.e.r; row++) { for (let col = range.s.c; col <= range.e.c; col++) { const cellAddress = XLSX.utils.encode_cell({ r: row, c: col }) if (row === 0) { // Bold header if (!worksheet[cellAddress]) continue worksheet[cellAddress].s = { font: { bold: true }, fill: { fgColor: { rgb: "3498db" } } } } } } // Adjust column widths worksheet['!cols'] = [ { width: 10 }, // ID { width: 30 }, // Product { width: 20 }, // Salesperson { width: 15 }, // Amount { width: 12 } // Date ] const workbook = XLSX.utils.book_new() XLSX.utils.book_append_sheet(workbook, worksheet, 'Data') XLSX.writeFile(workbook, filename) } 
Enter fullscreen mode Exit fullscreen mode

2. PDF with custom logo

// utils/pdfGenerator.js import pdfMake from 'pdfmake/build/pdfmake' export function generatePDFWithLogo(data, logoBase64) { const docDefinition = { content: [ // Logo and header { columns: [ { image: logoBase64, // Base64 image width: 100, height: 50 }, { text: 'COMPANY XYZ\nManagement Report', style: 'headerWithLogo', alignment: 'right' } ] }, // Rest of content... ], styles: { headerWithLogo: { fontSize: 14, bold: true, color: '#2c3e50' } } } return pdfMake.createPdf(docDefinition) } 
Enter fullscreen mode Exit fullscreen mode

Main page integrating everything

indexPage.vue

Replace the contents of the src/pages/indexPage.vue file, use the contents;

<template> <q-page class="q-pa-md"> <div class="text-h4 q-mb-lg text-center"> 📊 Demo: PDF & Excel on Frontend </div> <div class="row q-gutter-md"> <div class="col-12 col-md-5"> <ExportExcel /> </div> <div class="col-12 col-md-5"> <ExportPDF /> </div> </div> <q-separator class="q-my-lg" /> <div class="text-center"> <p class="text-body1"> ✨ No backend, no complications! ✨ </p> <p class="text-caption text-grey-7"> 100% browser file generation using Vue.js + Quasar </p> </div> </q-page> </template> <script> import ExportExcel from 'components/ExportExcel.vue' import ExportPDF from 'components/ExportPDF.vue' export default { name: 'ExportDemo', components: { ExportExcel, ExportPDF } } </script> 
Enter fullscreen mode Exit fullscreen mode

Conclusion

With these implementations, you have a complete export system that:

  • Works offline - Doesn't depend on server
  • High performance - Client-side processing
  • Flexible - Easy to customize styles and layouts
  • Professional - Good-looking PDFs and Excel files
  • Scalable - Can be integrated into any Quasar project

Next steps:

  • Implement charts in PDF using Chart.js
  • Add multiple sheets in Excel
  • Create reusable templates
  • Integrate with upload components for logos

🔗 Useful links

- GitHub project files

💡 Tip: Save this tutorial and try out the code! The best way to learn is by practicing.

🚀 Enjoyed the content? Share with other developers and leave your feedback in the comments!

Top comments (0)