For humans and agents
How to read these docs as a person, and what a coding agent (Claude Code, Cursor, Codex) should load before writing glyphline code.
For humans
Start with the demo on the introduction, then pick your framework: React or Svelte. Each has an overview, a PDF page component, selection and marks, and an API page. The playground shows every option live, and motion and accuracy explain how it works. Every page has Copy Markdown and Open buttons at the top if you want to paste a page into a chat.
For agents
The same docs as plain Markdown, built with the site:
| URL | What |
|---|---|
/llms.txt | index of every page with a one-line summary |
/llms-full.txt | all pages in one file (about 45 KB) |
/llms.mdx/docs/<page>/content.md | one page, e.g. /llms.mdx/docs/react/content.md |
node_modules/glyphline/AGENTS.md | the rules below, shipped inside the npm package |
Point your agent at them, for example:
Before using glyphline, read node_modules/glyphline/AGENTS.md
and https://glyphline.lucaspiera.com/llms-full.txtIn Claude Code or Codex, put that line in your project's CLAUDE.md or AGENTS.md.
The rules in AGENTS.md
What it is
An <svg> overlay that draws text highlights over text it does not own: a sentence outline, a word
cursor that animates between words, the user's selection and saved marks. Text comes from a
TextSource: domSource (HTML) or pdfSource (a pdf.js page).
Imports (one package, subpaths)
import { createHighlighter, wordAtPoint } from 'glyphline'; // core
import { domSource } from 'glyphline/dom'; // HTML
import { pdfSource } from 'glyphline/pdf'; // pdf.js page; needs pdfjs-dist
import { GlyphOverlay, useDomSource, useAsyncSource } from 'glyphline/react'; // React 18/19
import { GlyphOverlay, domText, asyncText, highlight } from 'glyphline/svelte'; // Svelte 5
import 'glyphline/style.css'; // default colours, import once
import 'glyphline/pdf.css'; // only if you render the pdf.js text layer without pdf_viewer.cssThere are no packages named react-glyphline or svelte-glyphline. Install only glyphline
(plus pdfjs-dist for PDFs).
Rules that are easy to get wrong
- The overlay must sit inside a positioned element (
position: relative) that wraps exactly the text.GlyphOverlayand thegl-overlayclass stretch the svg over that parent. - Indices (
word,sentence) are intosource.words/source.sentencesof that source. After the text changes, use a new source (useDomSource/domTextrebuild it for you). - Targets can also be character ranges
{ start, end }(end exclusive) intosource.text. - Sources read the DOM or pdf.js: create them in the browser only (effects, refs, attachments),
never during SSR. The React entry is marked
"use client". - PDF: call
getDocument({ url, fontExtraProperties: true }). Without it the glyph positions fall back to a less precise method. PassOPSfrompdfjs-disttopdfSource. - PDF boxes are in page units at scale 1; the svg
viewBoxis the page size. Do not multiply by the zoom yourself: size the page element and the overlay scales. - For selections inside a PDF, call
source.withTextLayer(textLayer.textDivs, layerEl, scale)after rendering pdf.jsTextLayer, and pass the returned source. - To draw the selection:
options={{ trackSelection: { snapToWords: true, onChange } }}; add classgl-native-selection-hiddento the text element to hide the browser's own colour. - A white PDF page inside a dark app:
theme="light"onGlyphOverlay. - Click to word:
wordAtPoint(source, svgElement, e.clientX, e.clientY)returns a word index or -1. Skip it whendocument.getSelection()?.isCollapsedis false (the user is selecting).
Minimal examples
React:
const [ref, source] = useDomSource<HTMLElement>();
<div style={{ position: 'relative' }}>
<article ref={ref}>…</article>
<GlyphOverlay source={source} sentence={s} word={w} />
</div>;Svelte 5:
<script lang="ts">
import { domText, GlyphOverlay } from 'glyphline/svelte';
const text = domText();
</script>
<div style="position: relative">
<article {@attach text.attach}>…</article>
<GlyphOverlay source={text.source} {sentence} {word} />
</div>No framework:
const hl = createHighlighter(svg, domSource(article), { trackSelection: true });
hl.update({ sentence: 0, word: 0 });
hl.destroy(); // when doneMade by Lucas Piera