For most Laravel PDF generation work, use DomPDF when the document is an invoice, receipt, or statement, Snappy when you need repeating headers and true page numbers on an existing layout, and Browsershot when the PDF has to match a modern web page built with flexbox, grid, or JavaScript charts. That is the short answer.
The choice usually gets made in the wrong order. A client needs invoices by Friday, someone installs whatever the first search result recommends, and six months later finance asks why page two lost its header and why the totals column drifts on long orders.
You have probably shipped a document that looked perfect on your machine and fell apart in the production container. This guide compares the three packages Laravel developers actually reach for, on the four things that decide the outcome: rendering engine, CSS support, infrastructure cost, and maintenance risk. You get install commands, working code for each, a side by side table, and the small abstraction that lets you swap engines later without rewriting a controller.
Key Takeaways
How Laravel PDF generation actually works
All three packages do the same job at a high level. They take HTML rendered from a Blade view and hand it to an engine that turns markup into pages. None of them draw PDF primitives the way TCPDF and FPDF do, so you write templates, not coordinates.
The engine is the whole decision. DomPDF runs a PHP renderer inside your application process. Snappy shells out to the wkhtmltopdf binary, which wraps an old QtWebKit build. Browsershot drives headless Chrome through Node and Puppeteer. CSS support, memory use, deploy complexity, and security exposure all follow from that one choice.
The Blade half stays identical whichever engine you pick.
// Any of the three packages starts from a normal Blade view.
return Pdf::loadView('pdf.invoice', ['invoice' => $invoice])
->setPaper('a4')
->download("invoice-{$invoice->number}.pdf");Because the template is a standard Blade view, you can point a route at it and iterate in the browser with hot reload, then render to PDF only when the layout is right. That one habit saves more time than any package choice.
If you would rather describe the document and get the Blade view, the controller, and the tests back as files you own, you can generate a production-ready Laravel app from a prompt and take it from there.
DomPDF is the fastest way to ship a Laravel PDF invoice
Reach for DomPDF first when the document is mostly text, tables, and a logo. Laravel DomPDF has no system dependency, installs in one command, and runs anywhere PHP runs, including shared hosting and slim containers.
composer require barryvdh/laravel-dompdf
php artisan vendor:publish --provider="Barryvdh\DomPDF\ServiceProvider"use Barryvdh\DomPDF\Facade\Pdf;
public function download(Invoice $invoice)
{
$this->authorize('view', $invoice);
$pdf = Pdf::loadView('pdf.invoice', [
'invoice' => $invoice->load('lines', 'customer'),
])->setPaper('a4');
return $pdf->download("invoice-{$invoice->number}.pdf");
}Note the authorization check on the first line. A billing document usually contains another company's financial data, so gate it with a Policy before you render anything.
What DomPDF handles well
Table based layouts, which is exactly how invoices and statements are built anyway.
Page setup through the
@pagerule, pluspage-break-inside: avoidto stop a line item splitting across pages.Custom fonts declared with
@font-facepointing at a file that already lives on disk.Inline images, including base64 data URIs for a logo, with no network call at render time.
Where DomPDF stops
There is no flexbox, no CSS grid, and no JavaScript. A designer hands you a Tailwind layout, and DomPDF renders it as a stack of unstyled blocks. Long tables also eat memory, because the whole document tree is held in PHP while it renders.
Page numbering is the other rough edge. Getting a running "Page 1 of 4" footer means enabling inline PHP in templates through the enable_php option, which turns any editable template into arbitrary code execution. Leave enable_php off. Leave enable_remote off too unless you truly need external assets, since remote resource loading has been the source of past DomPDF advisories.
A two person team building a B2B billing product hit this exactly. They shipped invoice downloads with DomPDF in an afternoon, table layout, inline logo, fixed A4 paper, and it held up for two years of e-commerce order receipts. Then a designer redesigned the header with flexbox, the PDF collapsed into a single column, and the team spent a day converting it back to nested tables. The engine did not fail. The template moved past what the engine supports.
Snappy gives you page numbers and an unmaintained binary
Snappy wraps the wkhtmltopdf command line tool. The rendering engine is a patched QtWebKit, roughly a Safari from 2011, which is a large step up from DomPDF and a large step behind Chrome.
composer require barryvdh/laravel-snappy
php artisan vendor:publish --provider="Barryvdh\Snappy\ServiceProvider"
# then point config/snappy.php at the wkhtmltopdf binary on the machineuse Barryvdh\Snappy\Facades\SnappyPdf;
$pdf = SnappyPdf::loadView('pdf.contract', ['contract' => $contract])
->setOption('footer-right', 'Page [page] of [topage]')
->setOption('margin-top', 20)
->setOption('disable-local-file-access', true);
return $pdf->inline('contract.pdf');Those [page] and [topage] placeholders are the reason Snappy survives in long document workflows. Contracts, policy packs, and multi page reports need a footer on every page, and wkhtmltopdf gives you that natively along with margins and a generated table of contents.
The catch is maintenance. The wkhtmltopdf repository was archived in 2023, and development stopped. You are pinning a binary that gets no security patches and no rendering fixes, on an engine that never supported flexbox or grid. Some distributions have already dropped the package, which pushes teams into hunting down old static builds.
Treat it as a shell command, because that is what it is. Keep disable-local-file-access on so a crafted template cannot read files off the server, and never pass user supplied HTML or URLs straight into it. The binary also has to exist on every server, in every container image, and in CI, which is why your deploy step matters as much as the package. A one-click deployment that provisions the same dependencies every time removes a whole class of "works locally, blank page in production" tickets.
Honest verdict for 2026. Pick Snappy if you already run it and it works, or if you need its footer and table of contents behaviour on a layout that does not require modern CSS. Do not start a greenfield project on it.
Browsershot renders whatever Chrome renders
Browsershot takes a different bet. It runs headless Chrome through Puppeteer, so the PDF is produced by the same engine your users already view the page in. Flexbox, grid, web fonts, SVG, and JavaScript charts all work, because nothing is being reimplemented.
composer require spatie/browsershot
npm install puppeteeruse Spatie\Browsershot\Browsershot;
$html = view('pdf.statement', ['statement' => $statement])->render();
Browsershot::html($html)
->format('A4')
->showBackground()
->margins(12, 10, 12, 10)
->showBrowserHeaderAndFooter()
->footerHtml("<div style='font-size:10px;width:100%;text-align:center'><span class='pageNumber'></span> of <span class='totalPages'></span></div>")
->waitUntilNetworkIdle()
->savePdf(storage_path("app/statements/{$statement->id}.pdf"));The pageNumber and totalPages classes come from Chrome's own print header and footer templates, so you get page numbering without enabling anything dangerous. If you want a friendlier API on top, spatie/laravel-pdf wraps Browsershot with a Blade first interface and Tailwind support.
The price is infrastructure. Every render needs Node, Puppeteer, and a Chromium install, plus the system fonts and shared libraries Chromium expects. Miss the font packages in your Docker image and text renders as boxes. Memory use is far higher than the other two, cold starts are measurable, and the common --no-sandbox workaround weakens process isolation, so untrusted HTML is a genuine risk.
An agency ran into the full arc on a quote builder. The web preview was Tailwind, DomPDF flattened it, and the client rejected the output. Moving to Browsershot made the PDF match the preview exactly, but the deploy image grew, the first render on a cold worker was slow enough to time out a web request, and generation had to move to a queued job with a dedicated worker. The document was right, and the architecture had to change to support it.
Laravel PDF generation compared side by side
Criteria | DomPDF | Snappy with wkhtmltopdf | Browsershot |
|---|---|---|---|
Engine | Pure PHP renderer | Patched QtWebKit binary | Headless Chrome via Puppeteer |
CSS support | CSS 2.1 plus limited CSS3 | 2011 era WebKit, partial CSS3 | Everything Chrome supports |
Flexbox and grid | No | No | Yes |
JavaScript | None | Limited, needs a render delay | Full |
Page numbers and running footers | Awkward, needs inline PHP | Native placeholders | Native Chrome templates |
System dependencies | None beyond PHP extensions | wkhtmltopdf binary | Node, Puppeteer, Chromium |
Resource cost | Lowest | Moderate | Highest |
Project status | Actively maintained | Archived in 2023 | Actively maintained |
Best fit | Invoices, receipts, statements | Existing long document workflows | Design heavy reports and charts |
Read that table as a constraint check, not a ranking. If the document is a table with a logo, the cheapest engine wins. If the document is a designed page, only Chrome will reproduce it. You can generate the whole billing feature from a prompt and still choose the engine yourself.
Keep Laravel PDF generation behind one interface
The teams that regret their choice are the ones that scattered a facade across fifteen controllers. Define a contract once, and the engine becomes a config value.
interface DocumentRenderer
{
/** Returns raw PDF bytes. */
public function render(string $view, array $data): string;
}Write a DomPdfRenderer and a BrowsershotRenderer, bind one in a service provider based on config, then inject the interface into a queued job so no user ever waits on Chrome.
class GenerateInvoicePdf implements ShouldQueue
{
public function __construct(public Invoice $invoice) {}
public function handle(DocumentRenderer $renderer): void
{
$bytes = $renderer->render('pdf.invoice', ['invoice' => $this->invoice]);
Storage::disk('s3')->put("invoices/{$this->invoice->uuid}.pdf", $bytes);
}
}Pushing rendering onto a queue buys you retries, a timeout that does not hit the web server, and the freedom to run heavy renders on a worker sized for them. The controller just dispatches and returns.
Then test the plumbing rather than the pixels. Assert the file lands and that the bytes start with the PDF magic marker.
it('stores a pdf for the invoice', function () {
Storage::fake('s3');
$invoice = Invoice::factory()->create();
(new GenerateInvoicePdf($invoice))->handle(app(DocumentRenderer::class));
Storage::disk('s3')->assertExists("invoices/{$invoice->uuid}.pdf");
});This scaffolding is worth generating rather than typing. StackMivo's AI code generation produces the interface, both drivers, the queued job, and the Pest test as standard Laravel files you can read and edit, and its Laravel-native intelligence keeps the output idiomatic instead of generic PHP. If you are still wiring up the models and factories behind these documents, the php artisan make:model cheatsheet covers the flags worth knowing.
Pick your Laravel PDF generation stack on constraints
Three rules cover almost every case. Text and tables in a fixed layout go to DomPDF, because zero dependencies beats every other advantage. Existing long documents that need running footers stay on Snappy, with the binary pinned and the knowledge that nobody is patching it. Anything a designer built in a browser goes to Browsershot, on a queue, with Chromium and fonts baked into the image.
What matters more than the package is the seam around it. One interface, one queued job, one storage path, and the engine becomes reversible. Teams that skip that step end up rewriting controllers the first time a template outgrows the renderer, which is the expensive version of a decision that should cost an afternoon.
Start with the simplest engine your document allows, measure a real render on production hardware before you commit, and give yourself room to trade up. Laravel PDF generation stops being a recurring problem the moment it becomes a swappable detail.
Build your Laravel app with AI
Skip the boilerplate. StackMivo turns a plain-English prompt into production-ready Laravel apps, with models, migrations, controllers, CRUD, auth, and tests.
Get started free