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?












