penciled is a 15 kB open-source JavaScript library for persistent highlights and notes on any web page. Think Sonner, for text annotations.
Browser support: Chrome/Edge 105+, Safari 17.2+, Firefox 140+; no fallback on older browsers, annotate() only warns. Details
Documentation · Playground · npm · Compare · Browser extension
npm install penciledimport { annotate } from 'penciled';
import 'penciled/styles.css';
annotate('#article');Readers can now select text to highlight it, add a note, or click a highlight to change it. Annotations are saved to localStorage and come back on reload.
- Beautiful by default. A floating toolbar and note editor that appear only when needed, in light and dark themes.
- Yours to style. Theme the UI with CSS custom properties, or restyle anything: default selectors have (almost) zero specificity, so your CSS wins. Need a different UI altogether? Go headless and build your own. See Theming.
- Framework-agnostic. Plain DOM, with an optional React adapter. Highlights are painted with the CSS Custom Highlight API, so your DOM is never modified.
- Reliable anchoring. Highlights are re-anchored when the content changes. When penciled can't be sure where a highlight belongs, it leaves it out instead of highlighting the wrong text.
- Persistent. Uses
localStorageout of the box. Plug in your own storage adapter to save to a backend. - Accessible. Keyboard shortcuts for the toolbar and for moving between annotations, with screen-reader announcements.
- Small. About 15 kB min+gzip with the UI, 6 kB headless. One dependency (
@floating-ui/dom).
Not building a site? penciled for the browser lets you highlight and take notes on any website, in Chrome, Edge and other Chromium browsers 123+ and Firefox 140+. Install it for Chrome, Firefox or Edge. It's built on this library and lives in apps/extension.
annotate('#article', {
documentId: 'post-42',
storage: {
load: (documentId) => fetch(`/api/annotations/${documentId}`).then((r) => r.json()),
save: (documentId, annotations) =>
fetch(`/api/annotations/${documentId}`, {
method: 'PUT',
body: JSON.stringify(annotations),
}).then(() => {}),
},
});Adapters can also implement create, update and delete to save one annotation at a time. See Custom storage.
import { Annotatable } from 'penciled/react';
import 'penciled/styles.css';
export function Article({ children }) {
return <Annotatable options={{ documentId: 'post-42' }}>{children}</Annotatable>;
}There is also a useAnnotations(ref, options) hook. See React.
penciled/headless is the engine without the UI. It handles anchoring, painting, persistence and events, and you build the interface with whatever components and styles you already use.
import { annotate } from 'penciled/headless';
const annotator = annotate('#article');
annotator.on('select', (context) => {
// Show your own UI for the selected text or annotation.
});Chrome and Edge 105+, Safari 17.2+ and Firefox 140+, on desktop and mobile. With the default styles, the minimums are Chrome and Edge 123+ and Safari 17.5+. In older browsers annotate() logs a warning and does nothing. Importing is safe during server-side rendering. See Browser support.
penciled (penciled.js) is a 15 kB open-source JavaScript library that lets readers highlight text and write notes on any web page. Call annotate('#article') and readers get a floating toolbar and a note editor. Annotations are saved to localStorage, or to your own backend through a storage adapter. It's MIT licensed and published on npm as penciled.
No. Highlights are painted by the browser with the CSS Custom Highlight API, so your text is never wrapped in <mark> or <span> elements, and frameworks can re-render the page as often as they like. The only elements penciled adds are its own toolbar, note editor and screen-reader live region, outside your content.
Yes. penciled/react has a useAnnotations() hook and an <Annotatable> component, and every entry point is safe to import during server-side rendering, in Next.js, Astro, Remix and others. Only annotate() needs a browser. With other frameworks, call annotate() on a mounted element and destroy() when it unmounts.
Yes. Pass a storage adapter with async load() and save() methods, and optionally create(), update() and delete() to save one annotation at a time. Annotations are plain JSON, and toW3C() converts them to W3C Web Annotations.
web-highlighter wraps each highlight in <span> elements and leaves the UI and storage to you. Recogito's Text Annotator is a larger framework built around the W3C Web Annotation model, with multi-user features, for research and collaborative annotation. penciled never changes your DOM, ships a toolbar, note editor and persistence, and is about 15 kB with the UI. In exchange it needs a browser with the CSS Custom Highlight API, while both of the others also work in older browsers. The comparison page at https://www.penciled.org/compare/ covers them and three more.
No. penciled.js is an open-source JavaScript library, at penciled.org. It is not related to Penciled, the healthcare company at penciled.com.
pnpm install
pnpm test
pnpm docs:devRead CONTRIBUTING.md before opening a pull request.
MIT © Marcel Losso

