Live. A real PDF, rendered by the viewer you would install.
Page through it with the toolbar, or with arrow keys once it has focus. Press F for fullscreen.
The whole contract is one div
The editor plugin writes this. The viewer reads it. Neither one knows the other exists, and this markup is the only thing they agree on.
<div class="slipsheet"
data-src="https://cdn.example.com/report.pdf"
data-pages="12"
data-filename="quarterly-report.pdf">
<a href="https://cdn.example.com/report.pdf"
download="quarterly-report.pdf">
Download quarterly-report.pdf (12 pages)
</a>
</div>
That nested link is the entire trick. It is what stays behind when JavaScript is off, when PDF.js fails to load, or when the viewer is never installed at all. The button above removes the viewer from the live demo so you can watch it happen instead of taking our word for it.
Install
Take one or take both. The viewer is useful with any editor, or none.
npm install @slipsheet/viewer @slipsheet/hugerte
Render stored content
import { Slipsheet } from '@slipsheet/viewer';
import '@slipsheet/viewer/style';
Slipsheet.init(); // hydrates every .slipsheet on the page
Author it in an editor
hugerte.init({ // or tinymce.init on TinyMCE 6
plugins: 'slipsheet',
toolbar: 'slipsheet',
slipsheet_upload_handler: async (file, progress) => {
const { url, pages } = await myUpload(file, progress);
return { src: url, pages, filename: file.name };
},
extended_valid_elements:
'div[class|data-src|data-pages|data-filename|contenteditable],a[href|download|target]',
convert_urls: false,
});
Both of those last two options are required. The first keeps the editor from stripping the embed's attributes on save; the second stops it rewriting the fallback link to a different path than the viewer reads.
The upload handler is yours. slipsheet never talks to a backend, and has no opinion about where your files live.
Why it is built this way
- Nothing depends on JavaScript
- Stored content is plain HTML with a working download link inside it. Every failure mode, including the total absence of this library, leaves the reader with something they can use.
- Neither half knows about the other
- The plugin cannot render and the viewer cannot upload. They share no runtime and no build step. Bring your own editor, or your own renderer, and the other piece still works.
- PDF.js is fetched, not bundled
- It loads on first use from a URL you control, so you get lazy loading and PDF.js security updates without waiting on a slipsheet release.
- Keyboard and screen reader are not an afterthought
-
Arrow keys, Home, End, and F for fullscreen.
Page changes announce through
aria-live. Motion respectsprefers-reduced-motion. Try it above.