npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lexical-footnote

v0.3.1

Published

GFM footnote extension for Lexical — footnote refs, definitions, in-page links, and mdast markdown round-trip

Readme

lexical-footnote

GFM footnotes for Lexical: superscript cues, an auto-managed notes section, in-page navigation, GitHub-compatible HTML export/import, and exact markdown round-trip via @lexical/mdast.

Status: beta, actively developed. Built on [email protected]'s experimental extension / mdast / DOM-import / named-slot APIs, which may break between releases — pin your lexical version. ROADMAP.md has what's planned.

Model

| Node | Kind | Role | | ------------------------ | ------------------------------ | ------------------------------------------- | | FootnoteRefNode | inline DecoratorTextNode | a cue — in the body, or inside another note | | FootnoteSectionNode | ElementNode, last root child | hosts the definitions in named slots | | FootnoteDefinitionNode | ElementNode, slot value | one note's flow content |

  • The slot map is the definition map. GFM keys definitions by identifier; so does the model — each definition is a named slot (fn:<id>) on the section. "One definition per identifier" is structural, there is no stored order to maintain, and reordering the body never mutates the document.
  • Derived numbering. Numbers follow GFM — body cues first, then whatever the notes themselves cite — recomputed each commit, exposed as a signal, never stored on a node.
  • Editable islands. Each note renders in its own contentEditable container: editable in place, structurally isolated from the body.
  • Self-healing. A dangling cue heals an empty definition; one section, pinned last; orphan definitions are kept (legal in GFM).

Because definitions are slot values, they don't appear in section.getChildren() and definition.remove() doesn't detach them — use $getFootnoteDefinitions() / $removeFootnoteDefinition(id).

Usage

import {FootnoteExtension} from 'lexical-footnote';
import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';
import {defineExtension} from 'lexical';

const appExtension = defineExtension({
  name: 'app',
  namespace: 'app',
  dependencies: [RichTextExtension, FootnoteExtension],
});

<LexicalExtensionComposer extension={appExtension}>…</LexicalExtensionComposer>;

Insert via command, output API, or by typing — a literal [^id] in body text becomes a cue and heals its definition (skipped in code-formatted text):

editor.dispatchCommand(INSERT_FOOTNOTE_COMMAND, undefined);
// or
const {insertFootnote, gotoDefinition, gotoRef, cleanupOrphans, numbers} =
  getExtensionDependencyFromEditor(editor, FootnoteExtension).output;

gotoRef(id); // a note cited more than once: gotoRef(id, 2) for its 2nd cue
// `numbers` is a signal of Map<footnoteId, number> — the displayed numbering

Markdown (optional)

import {FootnoteMdastExtension} from 'lexical-footnote/mdast';
// add alongside MdastCommonMarkExtension / MdastExportExtension

[^id] / [^id]: … round-trips through @lexical/mdast. Definitions declared anywhere in the source are normalized into the section.

Clipboard (optional)

import {FootnoteClipboardExtension} from 'lexical-footnote/clipboard';

Copy carries the notes a selection references — nested citations included — appended to text/html as a GFM footnote section and to the Lexical JSON payload, so a cue pasted elsewhere brings its text along.

Paste routes text/html through rule-based import (the default paste path never consults it), so footnote-bearing HTML pastes as real footnotes. Recognized and verified in Chrome, Firefox, and Safari:

  • Word — desktop and web, including Safari's sanitized clipboard
  • Google Docs — exported/published HTML (in-editor copies don't carry definitions — a Docs limitation — and paste as cues with empty notes)
  • GitHub's rendered GFM, and this package's own exportDOM

Source chrome (separators, literal markers, backrefs) is stripped, and a paste can never clobber existing notes: Word/Docs footnotes get fresh ids, and a carried footnote whose id names a different note in this document is re-keyed on arrival. Same-content pastes keep their id, so cutting a cue and pasting it back reconnects, and multiple cues keep sharing one note.

HTML

exportDOM emits GitHub's exact shape: <sup><a data-footnote-ref id="fnref-id"> cues (repeat cues suffixed fnref-id-2), a <section data-footnotes> with a visually-hidden footnote-label heading, and one data-footnote-backref link per cue (, ↩²). Import accepts both this output and GitHub's user-content- prefixed variant.

One deliberate difference: definitions nothing refers to are still exported — this is a document format, and an orphan note is content someone wrote. If your HTML is a rendering rather than a document, opt into GitHub's behavior (markdown export is unaffected either way):

configExtension(FootnoteExtension, {dropOrphansOnExport: true});

Styling (optional)

import 'lexical-footnote/styles.css';

The default look for the footnote anatomy — superscript cues, the short separator rule, the numbered notes list, the backrefs. Host-agnostic: it inherits your typography and colors, sizes in em, and works on any light or dark background with zero configuration. Re-skin it by setting a handful of custom properties (all optional):

| Token | What it colors | | ------------------------------------ | ------------------------------------------------------------------------------- | | --lexical-footnote-accent | cue numbers, list markers, backrefs, focus fills (falls back to currentColor) | | --lexical-footnote-accent-contrast | text on the solid focus/selected fill (falls back to the Canvas system color) | | --lexical-footnote-note-color | the notes section's text (falls back to a softened currentColor) | | --lexical-footnote-rule-color | the separator rule above the notes (falls back to a faint currentColor) |

Skipping the stylesheet entirely and styling the class names / data-attributes yourself is equally supported — nothing in the runtime depends on it.

Behavior notes

  • Inserting keeps selected text, places the cue after it, and moves the caret into the new note (Word/tiptap semantics).
  • Deleting a cue keeps its note as a recoverable orphan; cleanupOrphans() discards orphans permanently. Deleting a note deletes its cues in the same update (one undo restores both). Emptying a note and deleting again removes it — the keyboard route to note deletion. Emptying the whole document removes the section too.
  • Backspace at a cue boundary selects the cue first; a second delete removes it.
  • Many cues may share one id (GFM): they share a number, and the note gets a backref per cue. A note may cite another note — it's numbered right after the note that cites it; cycles and self-citation terminate.
  • Arrow keys move between notes in every browser. Each note is an editable island, and Firefox won't carry the caret across one on its own.

vs. tiptap Pages footnotes

tiptap renders footnotes per page — a feature of its pagination engine, with content stored outside the document. Lexical has no page concept; a continuous document collects notes at the end, which tiptap calls endnotes. This package is that shape with GFM semantics and in-document definitions.

Development

pnpm install
pnpm dev            # vite demo
pnpm test           # headless (happy-dom): model, import/export
pnpm test:browser   # Chromium, Firefox, WebKit: caret behavior
pnpm build          # tsdown

The source splits along the same line as the tests: model/ is the rules (headless, no DOM), ui/ needs a real browser, io/ is the contracts with other formats, spike/ pins the upstream slot APIs this package relies on.

License

MIT