Deleting pages from a PDF is one of those features that sounds trivial until you try to build it. Users expect range input (1, 3, 5-7), a visual grid, instant feedback, and zero server involvement.
I built en.sotool.top/delete-pages/ to do exactly that. Here's how it works with Vue 3 and pdf-lib.
Why Client-Side?
PDFs often contain sensitive information. Contracts, medical records, financial statements. Even a "simple" page deletion tool should not force users to upload files to a server.
Client-side benefits:
- No upload bandwidth or size limits
- No server storage or cleanup
- Instant processing for normal files
- Works offline after the page loads
The tradeoff is that everything has to run in the browser, which limits the libraries you can use.
The Stack
- Vue 3 — UI and state
- pdf-lib — Load, manipulate, and save PDFs
- File API — Read the uploaded file
- lucide-vue-next — Icons
npm install pdf-lib
Loading the PDF and Counting Pages
First, read the file into an ArrayBuffer and load it with pdf-lib.
import { PDFDocument } from 'pdf-lib'
const pdfFile = ref<File | null>(null)
const totalPages = ref(0)
async function handleFile(files: File[]) {
if (files.length === 0) return
pdfFile.value = files[0]
const bytes = await files[0].arrayBuffer()
const pdf = await PDFDocument.load(bytes)
totalPages.value = pdf.getPageCount()
}
Now we know how many pages exist and can show the deletion UI.
Two Deletion Modes
I offer two ways to choose pages: by range and by visual selection.
Mode 1: Page Range Input
Users type something like 1, 5-7, 12. I parse it into a set of page numbers.
function parseDeletePages(input: string, max: number): Set<number> {
const set = new Set<number>()
const parts = input.split(',').map(s => s.trim())
for (const part of parts) {
if (part.includes('-')) {
const [start, end] = part.split('-').map(Number)
for (let i = start; i <= end && i <= max; i++) {
set.add(i)
}
} else {
const n = Number(part)
if (n >= 1 && n <= max) set.add(n)
}
}
return set
}
This handles single pages, ranges, and mixed input. Page numbers are 1-based because that is what users expect.
Mode 2: Visual Grid Selection
A grid of page number buttons. Click to toggle. Red means selected for deletion.
<div class="grid grid-cols-5 sm:grid-cols-8 gap-2">
<button
v-for="n in totalPages"
:key="n"
:class="selectedPages.has(n) ? 'bg-red-50 text-red-600' : 'hover:border-primary/40'"
@click="togglePage(n)"
>
{{ n }}
</button>
</div>
const selectedPages = ref<Set<number>>(new Set())
function togglePage(n: number) {
const s = new Set(selectedPages.value)
if (s.has(n)) s.delete(n)
else s.add(n)
selectedPages.value = s
}
I also added Select All, Invert, and Clear helpers for convenience.
Removing Pages with pdf-lib
Once we know which pages to delete, the logic is:
- Load the original PDF
- Create a new empty PDF
- Copy only the pages we want to keep
- Save and download
async function deletePages() {
if (!pdfFile.value) return
const bytes = await pdfFile.value.arrayBuffer()
const pdf = await PDFDocument.load(bytes)
const pageCount = pdf.getPageCount()
const toDelete = deleteMode.value === 'range'
? parseDeletePages(rangeInput.value, pageCount)
: selectedPages.value
if (toDelete.size === 0) {
alert('No pages selected')
return
}
if (toDelete.size === pageCount) {
alert('Cannot delete all pages')
return
}
const newPdf = await PDFDocument.create()
const keepIndices: number[] = []
for (let i = 0; i < pageCount; i++) {
if (!toDelete.has(i + 1)) keepIndices.push(i)
}
const pages = await newPdf.copyPages(pdf, keepIndices)
pages.forEach(p => newPdf.addPage(p))
const blob = new Blob([await newPdf.save()], { type: 'application/pdf' })
downloadBlob(blob, 'deleted_pages.pdf')
}
copyPages preserves the content of the kept pages without re-rendering them, so quality stays intact.
Downloading the Result
A small helper to trigger a file download from a Blob.
function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}
Lessons Learned
1-based indexing everywhere in the UI. Developers think in 0-based arrays. Users think in page numbers. Keep the internal logic 0-based but expose 1-based numbers everywhere the user sees.
Guard against deleting all pages. It is technically possible to create an empty PDF, but it is almost never what the user wants. Show a clear error instead.
Parse input defensively. Users will type 5 - 7, 5—7, or 5,6,7. Strip whitespace, handle dashes, ignore empty parts, and clamp to valid page numbers.
Preserve quality with copyPages. Do not re-encode the entire PDF. Copy only the pages you keep and let pdf-lib handle the rest.
Try It
The tool is live at en.sotool.top/delete-pages/.
Free, no signup, nothing uploads to a server.
Full source is on GitHub. The delete-pages logic is in src/views/DeletePages.vue.
Want More Advanced PDF Tools?
If you need OCR, form editing, digital signatures, or batch processing, Wondershare PDFelement is a solid desktop option. It keeps everything local.
This post contains affiliate links.
Have you built PDF manipulation tools in the browser? What edge cases did you run into?
Top comments (2)
How do you handle cases where the PDF is encrypted or has certain permissions set, does pdf-lib provide any built-in support for these scenarios?
Good question. We handle it at load time with
pdf-lib'signoreEncryptionoption:This lets us load PDFs that have a permissions password — the kind that prevents editing or printing but still allows the file to open. Since we're only adding page numbers to a file the user already owns, this is the right trade-off.
It does not bypass an open password. If the PDF requires a password just to view it, the user has to unlock it first.
Also worth noting: everything stays client-side, so even if we load a permissions-protected file, it never uploads to a server.
Suggested reply tags: none
Tone: helpful, technical