Splitting a PDF is one of those features that sounds trivial until you try to build it. Users expect range input (1-3, 5, 7-9), a per-page option, multiple file downloads, and zero server involvement.
I built en.sotool.top/split/ 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" splitting 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 split UI.
Two Split Modes
I offer two ways to split: by range and per page.
Mode 1: Page Range Input
Users type something like 1-3, 5, 7-9. I parse it into groups of page indices.
function parseRanges(input: string, max: number): number[][] {
const groups: number[][] = []
const parts = input.split(',').map(s => s.trim())
for (const part of parts) {
if (part.includes('-')) {
const [start, end] = part.split('-').map(Number)
const pages = []
for (let i = start; i <= end && i <= max; i++) {
pages.push(i - 1)
}
if (pages.length) groups.push(pages)
} else {
const n = Number(part)
if (n >= 1 && n <= max) groups.push([n - 1])
}
}
return groups
}
This handles ranges, single pages, and mixed input. Page numbers are 1-based because that is what users expect. Internal indices are 0-based for pdf-lib.
Mode 2: Per Page
Save every page as its own PDF. This is useful when each page is effectively a separate document.
Splitting with pdf-lib
Once we know which pages go where, the logic is:
- Load the original PDF
- For each output group, create a new empty PDF
- Copy the relevant pages
- Save and download
async function splitPdf() {
if (!pdfFile.value) return
const bytes = await pdfFile.value.arrayBuffer()
const pdf = await PDFDocument.load(bytes)
const pageCount = pdf.getPageCount()
if (splitMode.value === 'size') {
for (let i = 0; i < pageCount; i++) {
const newPdf = await PDFDocument.create()
const [page] = await newPdf.copyPages(pdf, [i])
newPdf.addPage(page)
const blob = new Blob([await newPdf.save()], { type: 'application/pdf' })
downloadBlob(blob, `page_${i + 1}.pdf`)
}
} else {
const ranges = parseRanges(rangeInput.value, pageCount)
for (let idx = 0; idx < ranges.length; idx++) {
const newPdf = await PDFDocument.create()
const pages = await newPdf.copyPages(pdf, ranges[idx])
pages.forEach(p => newPdf.addPage(p))
const blob = new Blob([await newPdf.save()], { type: 'application/pdf' })
downloadBlob(blob, `split_${idx + 1}.pdf`)
}
}
}
copyPages preserves the content of the copied 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)
}
When splitting by range, this runs once per output group. When splitting per page, it runs once per page.
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.
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.
Allow multiple downloads. Splitting produces more than one file. Make sure your download helper works in a loop and does not block the browser.
Preserve quality with copyPages. Do not re-encode the entire PDF. Copy only the pages you need and let pdf-lib handle the rest.
Watch for browser pop-up blockers. Multiple automatic downloads can trigger the blocker. Some users may need to allow downloads from your domain.
Try It
The tool is live at en.sotool.top/split/.
Free, no signup, nothing uploads to a server.
Full source is on GitHub. The split logic is in src/views/Split.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?




















