penciled

Read with a pencil in hand.

Highlights and notes for any webpage. 15 kB, no backend, never touches your DOM.

npm install penciled
Read the docs

penciled adds highlights and notes to any page. Select a few words in this paragraph and a small toolbar appears: pick a color, or write a note. Nothing else about the page changes.

Your annotations are saved in this browser as you make them. Reload the page and they come back exactly where you left them, even if the text around them has moved a little. If a passage changes so much that penciled can't be sure where it went, the annotation is set aside rather than attached to the wrong words.

The highlights you see are painted by the browser itself, with the CSS Custom Highlight API. Your markup is never wrapped or rewritten, so frameworks can re-render the page as often as they like.

Select any text. Hover or click a highlight. Reload, and it's all still here.

Setup

Three lines. Highlights, notes and saving work immediately.

import { annotate } from 'penciled';
import 'penciled/styles.css';
annotate('#article');

Colors

Five presets that work in light and dark mode, plus any of your own.

Yellow, green, blue, pink and purple come built in, and mint is one of your own.
annotate('#article', {
colors: [
'yellow', 'green', 'blue', 'pink', 'purple',
{ name: 'mint', light: '#5eead480', dark: '#2dd4bf47' },
],
});

Notes

Plain-text notes on any highlight. Choose how readers discover them.

A note opens when you click its highlight.
Default
An underline shows which highlights have a note.
noteIndicator: 'underline'
Rest the mouse on this highlight to read it.
notePreview: 'hover'

Custom actions

Add your own buttons to the toolbar. Select a few words below and press the sparkle.

A custom action receives the selected text and can create a highlight, write a note or do anything else your app needs.
const askAI = {
id: 'ask-ai',
label: 'Ask AI',
icon: sparkles,
onSelect: async ({ range, annotation }) => {
const a = annotation ?? penciled.create(range, { color: 'purple' });
const answer = await explain(a.quote); // your endpoint
penciled.update(a.id, { note: answer });
},
};
const penciled = annotate('#article', {
actions: ['highlight', 'note', askAI, 'remove'],
});

Make it yours

The toolbar and note editor are themed with CSS custom properties, and the default selectors have (almost) zero specificity, so your CSS wins. Pick a look: every toolbar on this page follows it.

Colors, corners, shadows and fonts are all yours to change. Select a few words here, or open the note on this highlight.
/* penciled/styles.css, plus five lines to match this site. */
:root {
--penciled-font: 'Geist Variable', sans-serif;
--penciled-bg: light-dark(#fff, #201f1d);
--penciled-border: light-dark(#e7e4de, #ffffff14);
--penciled-accent: light-dark(#1c1b19, #ece9e4);
--penciled-accent-fg: light-dark(#fbfaf8, #141413);
}

Need something else entirely? penciled/headless is the engine without the UI, about 6 kB, so you can build the toolbar from your own components.Theming guide · Headless

Storage

localStorage by default: the demo above is saved underpenciled:site-demo. Two async methods save to your own API instead.

const url = (id) => `/api/notes/${id}`;
annotate('#article', {
documentId: 'post-42',
storage: {
load: (id) => fetch(url(id)).then((r) => r.json()),
save: (id, annotations) =>
fetch(url(id), { method: 'PUT', body: JSON.stringify(annotations) }),
},
});

React

A hook and a component. Re-render as often as you like.

import { Annotatable } from 'penciled/react';
import 'penciled/styles.css';
export function Post({ post }) {
return (
<Annotatable as="article" options={{ documentId: post.id }}>
<h1>{post.title}</h1>
<Markdown>{post.body}</Markdown>
</Annotatable>
);
}

Never touches your DOM

Highlight anything in this sentence and watch its markup below. It doesn't change.

Your markup stays exactly as you wrote it.