Overview

Diffs is a library for rendering code and diffs on the web. This includes both high-level, easy-to-use components, as well as exposing many of the internals if you want to selectively use specific pieces. We've built syntax highlighting on top of Shiki which provides a lot of great theme and language support.

We have an opinionated stance in our architecture: browsers are rather efficient at rendering raw HTML. We lean into this by having all the lower level APIs purely rendering strings (the raw HTML) that are then consumed by higher-order components and utilities. This gives us great performance and flexibility to support popular libraries like React as well as provide great tools if you want to stick to vanilla JavaScript and HTML. The higher-order components render all this out into Shadow DOM and CSS grid layout.

Generally speaking, you're probably going to want to use the higher level components since they provide an easy-to-use API that you can get started with rather quickly. We currently only have components for vanilla JavaScript and React, but will add more if there's demand.

For this overview, we'll talk about the vanilla JavaScript components for now but there are React equivalents for all of these.

Rendering Diffs

Our goal with visualizing diffs was to provide some flexible and approachable APIs for how you may want to render diffs. For this, we provide a component called FileDiff.

There are two ways to render diffs with FileDiff:

  1. Provide two versions of a file or code snippet to compare
  2. Consume a patch file

You can see examples of these approaches below, in both JavaScript and React.

Merge conflict resolution UI

Render conflicts through a dedicated diff primitive that treats current and incoming sections as structured additions/deletions without running text diffing. Resolve by choosing current, incoming, or both changes and preview the updated file instantly.

Installation

Diffs is published as an npm package. Install Diffs with the package manager of your choice:

Package Exports

The package provides several entry points for different use cases:

PackageDescription
@pierre/diffsVanilla JS components, plus utility functions
@pierre/diffs/reactReact components for rendering diffs and files
@pierre/diffs/editLow-level edit mode Editor for attaching editing to rendered file and diff components
@pierre/diffs/ssrServer-side rendering utilities for pre-rendering diffs with syntax highlighting
@pierre/diffs/workerWorker pool utilities for offloading syntax highlighting to background threads

Build with agents

Agent skill

Install the diffs agent skill with the Skills CLI for access to the entire API and common recipes, regardless of your integration method.

Prompt an agent

Alternatively, you can copy-paste this prompt to your agent to install the skill and point it to these docs as plain-text.

The "For Agents" button on the home page copies the same prompt.

Plain-text docs

Our documentation is also available in condensed, Markdown-formatted plain text, available in two versions:

  • llms.txt is a short index of the sections
  • llms-full.txt is the everything in the docs — prose, API tables, and code examples

Copy and paste as needed, or provide the URLs to your agent.

Core Types

Before diving into the components, it's helpful to understand the core file, diff, and annotation data structures used throughout the library.

FileContents

FileContents represents one existing file version. Use it when rendering a file with the <File> component, or pass it as oldFile and/or newFile to diff components. For added or deleted files, pass null for the intentionally missing side.

An omitted side is not the same as null. If you provide either oldFile or newFile, provide the other side too, using null only when that file side does not exist. Empty file contents are still a real file; represent them with contents: '', not null.

For rendering and Worker Pool caching, cacheKey is optional; when provided, treat it as a revision identity and change it with the contents, filename, language, or revision.

FileDiffMetadata

FileDiffMetadata represents the differences between file versions. It contains the hunks (changed regions), line counts, and optionally the full file contents for expansion (if possible).

When a component uses loadDiffFiles, treat FileDiffMetadata as mutable render metadata. A partial metadata object parsed from a patch can be upgraded in place: isPartial flips to false, hunks and line arrays are replaced with hydrated values, and the object identity is preserved.

If you reparse a patch or create a new partial FileDiffMetadata, the renderer treats it as a fresh partial model. Keep the same metadata object stable when you want hydration to persist.

When loaded files provide cache keys, hydration uses those keys so full-file highlights can be reused across diffs. Change each FileContents.cacheKey whenever the loaded contents, filename, language, or revision changes. If loaded files are unkeyed but the partial metadata has a cacheKey, hydration appends a hydrated segment as a fallback.

Tip: You can generate FileDiffMetadata using parseDiffFromFile (from file contents) or parsePatchFiles (from a patch string).

LineAnnotation and DiffLineAnnotation

LineAnnotation<T> places content on a file line and contains lineNumber plus typed metadata. Metadata is required when T is a concrete type and omitted when T is undefined. DiffLineAnnotation<T> adds side: 'additions' | 'deletions' to select a file side. Line coordinates are one-based on the selected file side, not row positions in the rendered diff. Use lineNumber: 0 for a file-level annotation above the first file line or, in a diff, above the first hunk or row on that side.

Callbacks shared by file and diff components use LineAnnotation[] | DiffLineAnnotation[]. Use isFileAnnotationCollection or isDiffAnnotationCollection to narrow the collection before reading shape-specific fields. For individual annotation unions, use isFileAnnotation or isDiffAnnotation.

Store a stable, position-independent application ID in metadata when an annotation owns drafts or other interactive state. For annotations that survive an edit, edit mode preserves metadata while remapping line coordinates. For the controlled update pattern and exact remapping rules, see Line annotations.

Creating Diffs

There are two ways to create a FileDiffMetadata.

From File Contents

Use parseDiffFromFile when you have the full file contents. Pass both sides for a changed file, oldFile: null for a new file, or newFile: null for a deleted file. This approach allows collapsed regions to be expanded.

From a Patch String

Use parsePatchFiles when you have a unified diff or patch file. This is useful when working with git output or patch files from APIs. Patch-derived metadata is partial until a renderer hydrates it with full files from loadDiffFiles.

Tip: If you need to change the language after creating a FileContents or FileDiffMetadata, use the setLanguageOverride utility function.

React API

Import React components from @pierre/diffs/react.

We offer a variety of components to render diffs and files. Many of them share similar types of props, which you can find documented in Shared Props.

Components

The React API exposes six main components:

  • CodeView renders a mixed, virtualized list of files and diffs inside one scroll container
  • MultiFileDiff compares file contents directly
  • PatchDiff renders from a patch string
  • FileDiff renders a pre-parsed FileDiffMetadata
  • File renders a single code file without a diff
  • UnresolvedFile renders merge conflict markers with built-in resolution UI
    • Currently in beta/experimental and may change in future releases.

For editing, mount one stable EditProvider high in the tree. Standalone components use edit, and optional editorOptions and editStateKey; CodeView uses item edit flags, editorOptions, and optional getEditStateKey. Forward the provider factory's document kind, options, and key to new Editor(). Each active component or item receives an independent editor. Two active editors of the same kind cannot share one key. UnresolvedFile is not editable. See Edit mode → Enable editing and CodeView → Editing for lifecycle and callback details.

editStateKey retains state only in memory. See Retain edit state in memory and Persist a draft across reloads for the distinction.

Keep non-primitive props stable across renders. Define static files, diffs, options, styles, and factories at module scope; when they depend on component state or props, use useMemo for objects and arrays and useCallback for functions. This applies to options, editorOptions, annotations, and render callbacks as well as file and fileDiff.

UnresolvedFile is intentionally uncontrolled in React. Treat file as initial input and remount (for example, with a changing key) when you want to reset.

MultiFileDiff accepts FileContents for each existing side. Pass oldFile={null} for a new file, or newFile={null} for a deleted file.

The CodeView tab above is the quick-start version. For the full guide on controlled items, imperative initialItems, ids, version, selection, and scrollTo, see CodeView.

Partial Diff Hydration

When loadDiffFiles is configured, partial FileDiffMetadata passed to FileDiff may be hydrated in place. Keep the same fileDiff object identity stable across parent rerenders when you want the hydrated full metadata to persist.

Return both sides for changed diffs and { oldFile: null, newFile } for pure renames. Added and deleted diffs do not need to be hydrated.

Passing a freshly parsed partial object resets hydration for that render. Avoid calling parsePatchFiles during every render before passing the result to FileDiff; store or memoize the parsed metadata instead.

Shared Props

The three diff components (MultiFileDiff, PatchDiff, and FileDiff) share a common set of props for configuration, annotations, and styling. The File component has similar props, but uses LineAnnotation instead of DiffLineAnnotation (no side property).

Every editable component (File, FileDiff, MultiFileDiff, PatchDiff) also takes onEditChange — the component's live change stream — and onEditComplete — the accept/reject boundary for content-changing edit sessions. Change events expose the attached editor; completion events expose the detached editor and final state during the callback. A missing completion handler rejects. Edit mode owns annotation positions during a session, so do not sync them back per keystroke; accepting installs the final file or diff, but you should make sure to hold onto the final versions provided to ensure you don't revert the changes. See Handle changes and completion and Line annotations.

CodeView reuses many of the same option names internally, but it has its own controlled items mode, imperative mode with optional initialItems, viewer ref, and mixed-item render props. See CodeView for the dedicated guide.

Header customization and collapsing behavior:

  • Use renderHeaderPrefix to render custom UI at the beginning of the built-in header, before the filename and icons, while keeping the default header layout.
  • Use renderHeaderFilenameSuffix for compact UI immediately after the displayed filename, such as badges, review state, or generated-file labels.
  • Use renderHeaderMetadata to render custom UI at the end of the built-in header, after the diff stats, while keeping the default header layout.
  • Use renderCustomHeader when you want to replace the built-in header content with your own custom designed one.
  • For diff components, these header callbacks receive fileDiff: FileDiffMetadata.
  • For File, the corresponding header callbacks receive file: FileContents.
  • Use options.collapsed to hide file body content while keeping the file header visible.

Post Render Lifecycle

options.onPostRender(node, instance, phase) is a DOM-node lifecycle callback. It fires with phase: 'mount' after the first committed render or hydration for a container node, phase: 'update' after later DOM-committing renders, and phase: 'unmount' before a mounted container node is removed, replaced, cleaned up, or recycled.

Use this callback when native DOM selection listeners need access to the rendered diff node or its shadow DOM. Attach listeners such as selectstart and selectionchange during mount, and remove them during unmount with teardown state captured by node.

Token callbacks (onTokenClick, onTokenEnter, onTokenLeave) and useTokenTransformer are documented in Token Hooks, including examples, payload details, performance notes, and Worker Pool caveats.

Vanilla JS API

Import vanilla JavaScript classes, components, and methods from @pierre/diffs.

Components

The Vanilla JS API exposes four core components: CodeView (render a mixed, virtualized list of files and diffs in one scroll container), FileDiff (compare file contents directly or render a pre-parsed FileDiffMetadata), File (render a single code file without a diff), and UnresolvedFile (render merge conflicts with built-in resolution controls). Start with these components for syntax highlighting, theming, layout, and interactivity.

UnresolvedFile is currently beta/experimental and may change in future releases.

See Edit mode → Vanilla JS for attaching Editor to a rendered File or FileDiff with edit(). Pass an optional editStateKey as the constructor's third argument for same-runtime retention.

UnresolvedFile in vanilla supports both uncontrolled and controlled callbacks (onMergeConflictResolve / onMergeConflictAction).

The CodeView tab above is the quick-start version. For the deeper guide on setup, setItems, addItems, getItem, removeItem, updateItem, selection, and scrollTo, see CodeView.

Props

Both FileDiff and File accept an options object in their constructor. The File component has similar options, but excludes diff-specific settings and uses LineAnnotation instead of DiffLineAnnotation (no side property).

Editable components take onEditChange (the live change stream) and onEditComplete (the accept/reject boundary for content-changing sessions) as FileOptions / FileDiffOptions props. Change events expose the attached editor; completion events expose the detached editor and final state during the callback. A missing completion handler automatically rejects. Edit mode owns annotation positions during a session, so do not sync them back per keystroke; accepting installs the final collection (just don't forget to grab references to the updated file/fileDiff and annotations if accepting). See Handle changes and completion and Line annotations.

When rendering direct file contents with FileDiff.render, pass FileContents for each existing side. Use oldFile: null for a new file, or newFile: null for a deleted file.

For partial diffs parsed from patches, pass loadDiffFiles to FileDiff constructor options when you want collapsed unchanged context to expand from full file contents. The loader receives the partial FileDiffMetadata and returns { oldFile, newFile }: changed and rename-changed diffs return both sides, while pure renames return { oldFile: null, newFile }. Added and deleted patch diffs do not need loader hydration. Components catch loader errors by default; set disableErrorHandling: true when you want errors to rethrow.

CodeView forwards many of those same options to each rendered item, while adding CodeView-specific controls like layout, itemMetrics, stickyHeaders, pointerEventsOnScroll, and smoothScrollSettings. Its class instance also exposes item-level methods such as addItems, getItem, removeItem, and updateItem. Editable items use createEditor(documentKind, options, editStateKey) and optional getEditStateKey(item). See CodeView for the dedicated guide and Retain edit state in memory for state inspection and clearing.

Header customization and collapsing behavior:

  • Use renderHeaderPrefix to render custom UI at the beginning of the built-in FileDiff header, before the filename and icon, while keeping the default header layout.
  • Use renderHeaderFilenameSuffix for compact UI immediately after the displayed filename, such as badges, review state, or generated-file labels.
  • Use renderHeaderMetadata to render custom UI at the end of the built-in FileDiff header, after the diff stats, while keeping the default header layout.
  • Use renderCustomHeader when you want to replace the built-in header content entirely.
  • In File, header callbacks receive file: FileContents.
  • Use collapsed in constructor options to hide file body content while keeping the file header visible.

Post Render Lifecycle

onPostRender(node, instance, phase) is a DOM-node lifecycle callback. It fires with phase: 'mount' after the first committed render or hydration for a container node, phase: 'update' after later DOM-committing renders, and phase: 'unmount' before a mounted container node is removed, replaced, cleaned up, or recycled.

Use this callback when native DOM selection listeners need access to the rendered diff node or its shadow DOM. Attach listeners such as selectstart and selectionchange during mount, and remove them during unmount with teardown state captured by node.

Token callbacks (onTokenClick, onTokenEnter, onTokenLeave) and useTokenTransformer are documented in Token Hooks, including examples, payload details, performance notes, and Worker Pool caveats.

Custom Hunk Separators

Start with the Hunk Separators section first. In most cases, styling the built-in separator markup with unsafeCSS is the better approach.

If that is still not enough, the low-level hunkSeparators(hunkData, instance) function remains available in Vanilla JS as a last-resort escape hatch. It is being phased out and is not the recommended path for new integrations, but the example below shows how it works when you truly need to render your own elements:

Renderers

For most use cases, you should use the higher-level components like FileDiff and File (vanilla JS) or the React components (MultiFileDiff, FileDiff, PatchDiff, File). These renderers are low-level building blocks intended for advanced use cases.

These renderer classes handle the low-level work of parsing and rendering code with syntax highlighting. Useful when you need direct access to the rendered output as HAST nodes or HTML strings for custom rendering pipelines.

DiffHunksRenderer

Takes a FileDiffMetadata data structure and renders out the raw HAST (Hypertext Abstract Syntax Tree) elements for diff hunks. You can generate FileDiffMetadata via parseDiffFromFile or parsePatchFiles utility functions.

FileRenderer

Takes a FileContents object (just a filename and contents string) and renders syntax-highlighted code as HAST elements. Useful for rendering single files without any diff context.

CodeView

CodeView is the high-level API for rendering one large scroll region that can contain files, diffs, or both.

CodeView renders a list of CodeViewItem[] and manages the hard parts for you: virtualization, measured layout reconciliation, sticky headers, selection across items, and scrollTo targeting by item, line, or absolute position.

You can check out a live demo at diffshub.com

If you need to render one or more files or diffs in a scrollable container, use CodeView to avoid handling scaling yourself.

What It Gives You

  • One scroll container for a mixed list of file and diff items.
  • Built-in per-line virtualization that should scale to nearly any file or diff that can fit in memory.
  • scrollTo APIs for items, line targets, and raw scroll positions.
  • Unified selection API, support for custom annotations, custom headers, and gutter utilities across the entire viewer.
  • Optional per-item edit mode for files and diffs.
  • Optional non-virtualized header and footer regions rendered inside the scroll container — ideal for PR summary cards and approval bars.

Core Model

CodeView is designed to enable easy rendering of any files or diffs, regardless of scale, so its data model does not depend on traditional immutability or deep equality checks, which can quickly become expensive.

  • Every item needs a stable unique id. That id is how scrollTo, line selection, getItem, removeItem, updateItem, and reconciliation find the correct records.
  • Items are either { type: 'file', file } or { type: 'diff', fileDiff }.
  • If you keep the same item id but change its content or annotations, you must increment the version so CodeView can make an efficient targeted updates based only on what changed without recomputing everything.
  • Selection is viewer-wide, meaning a selection in one file will remove the selection in another file in the same scroll view. The payload shape is { id, range } instead of only a line range.
  • The collapsed property on an item controls whether file or diff content is shown. You'll have to wire up your own custom header or utilities if you want to control it interactively. Remember to update version when this value changes.
  • The edit property enables edit mode for an item when React CodeView has an EditProvider, or vanilla CodeView has a createEditor option. Update version when toggling it.
  • CodeView-level options such as layout, itemMetrics, stickyHeaders, pointerEventsOnScroll, and smoothScrollSettings allow you to configure the scroll view. All other options are shared between all files and diffs.
  • loadDiffFiles is one of those shared diff options. It applies to diff items rendered inside CodeView, which is useful for large patch-driven review UIs where full file contents should be fetched only when users expand unchanged context. Hydration updates the existing fileDiff object in place, so keep its identity stable when the hydrated metadata should persist across later renders.

Editing

React CodeView gets its editor factory from the nearest EditProvider; unlike vanilla CodeView, it does not accept createEditor directly or inside options. Keep the provider mounted, set edit: true on the items that should be editable, and pass creation-time item-editor behavior through editorOptions. onItemEditChange(event, item) reports each live change with the owning item and its attached editor. onItemEditComplete(event, item, nextItem) is the accept/reject boundary for content-changing sessions. Return 'accept' to install nextItem; a controlled component must also mirror it into its own state. Return 'reject' to restore the latest external item. A missing handler will automatically reject.

Collapsing or scrolling an item out of view suspends its session rather than completing it. Disabling editing, removing an item, reset(), cleanUp(), and viewer unmount end changed sessions. Removal and teardown callbacks can capture the result.

Each edited item receives an independent editor whose document, history, selections, and horizontal code scroll survive virtualization. Per-item editors never own or restore CodeView's shared vertical position. Changes to the provider factory or editorOptions do not disturb active sessions; their latest values apply the next time an item enters edit mode.

Use getEditStateKey(item) to retain a resumable session after editing ends. Said another way, next time you edit the file in the same session, undo history, selection state and caret position will be preserved.

React passes the resolved key through its EditProvider; vanilla CodeView passes it to createEditor as the third argument. The resolver runs only when the item editor is created, so changing it does not re-key an active editor. CodeView will not derive this key for you.

Edit state retention is bounded and in memory only. Accepting or rejecting an item does not clear its keyed state. See Retain edit state in memory for EditStateManager, clearing, and durable persistence guidance.

For vanilla CodeView, keep using CodeViewOptions.createEditor. It exposes the same item-aware callbacks, and CodeView owns each returned editor's lifecycle.

Autofocus on Attach

Autofocus is opt-in per edit session. In React, pass a stable editorOptions object whose onAttach callback targets the first editable row with a visible top edge:

const editorOptions: EditorOptions<ThreadMetadata> = {
  onAttach(editor) {
    editor.focus({ lineNumber: 'first-visible', preventScroll: true });
  },
};

<CodeView items={items} editorOptions={editorOptions} />;

For vanilla CodeView, add the callback while constructing each item editor:

const viewer = new CodeView({
  getEditStateKey(item) {
    return `review:${item.id}`;
  },
  createEditor(documentKind, options, editStateKey) {
    return new Editor(
      documentKind,
      {
        ...options,
        onAttach(editor) {
          // This would force an initial line focus when edit is enabled on
          editor.focus({ lineNumber: 'first-visible', preventScroll: true });
        },
      },
      editStateKey
    );
  },
});

preventScroll: true preserves the viewer's scroll position. CodeView keeps the editor alive while an item is recycled, so the callback does not steal focus again when that item re-enters the virtualized window. If several items start an autofocusing edit session together, the last callback to run owns focus. See Control the editor for explicit line targets, viewport fallback, offsets, and selection-state behavior.

Padding & Gap

For controlling layout inside and between items in CodeView, you can use the layout prop. Unlike itemMetrics, these values actually set internal values and adjust the layout. You should not apply these values with CSS yourself.

Use the renderCodeViewHeader and renderCodeViewFooter options to render your own element at the start and end of the scroll content; before the first item and after the last one.

  • Headers and footers are not virtualized. Unlike items, they are always in the DOM while the viewer is mounted.
  • They are rendered inside the scroll container, as part of the scrollable content. CodeView doesn't apply any positioning of its own, and they don't affect stickyHeaders behavior for item headers.
  • You never declare a height. CodeView measures the element on mount and tracks later size changes with a ResizeObserver, so async content, font loads, and late-arriving React portals stay coherent, and scroll position is re-anchored when a header's height changes.
  • Both render even when the item list is empty, which makes them useful for loading or empty states in review UIs.
  • In React, return plain JSX from the renderCodeViewHeader / renderCodeViewFooter props. The node is portaled into a host element the viewer manages, so state-driven updates just work. Memoize the callbacks with useCallback (listing any state they read as deps) so the header and footer don't re-render on every parent render (don't trust React Compiler).
  • In Vanilla JS, return the same element across calls and mutate it in place to update; returning undefined empties the host. The callback's presence controls whether the host element exists at all.
  • The host elements carry data-diffs-code-view-header and data-diffs-code-view-footer attributes for styling, and are exposed via getHeaderElement() / getFooterElement() on the instance.

File & Diff Size Estimation

CodeView uses a line-based virtualization system that renders a minimal snapshot to keep browser performance top of mind. Under the hood, it estimates the mathematical size of all code, then corrects and caches those estimates as you scroll and more content renders. These estimates are based on itemMetrics, and can be verified with the __devOnlyValidateItemHeights property.

Examples

React Item Ownership

React CodeView supports two item ownership models. Use one per mounted viewer; do not switch between them without remounting with a new key.

ModeUseItem propItem updates
ControlledReact state owns the complete item listitemsPublish a new items array. Append-only changes are optimized; other changes reconcile the list.
ImperativeThe viewer instance owns the item list after mountoptional initialItemsUse the ref APIs: addItems, getItem, removeItem, and updateItem.

Use controlled mode when item data already lives naturally in React state and the list is small enough that mutating arrays or items is cheap. Use imperative mode for very large or streaming lists where routing every item update through React would be expensive. In imperative mode, omit items, optionally seed the viewer with initialItems, and use the CodeViewHandle to add new items, remove items, or update existing ones.

Editing Item Annotations

While an item is editing, edit mode owns its annotation positions and re-renders the annotation rows as the document changes — you should not sync them back through onItemEditChange. event.lineAnnotations on that callback reports the current collection if you want to observe it. A genuinely new item.annotations collection, published with a version bump, replaces the editor's internal remaps and treats its coordinates as positions in the current document. Defer external annotation updates unless you have recomputed their positions against that document.

The callback annotation type is LineAnnotation[] | DiffLineAnnotation[]: file items emit the side-less shape and diff items emit annotations with a side. Use isFileAnnotationCollection or isDiffAnnotationCollection to narrow it before reading shape-specific fields.

On completion, the nextItem CodeView builds already carries the final annotation collection (event.lineAnnotations) alongside the accepted file/fileDiff — returning 'accept' installs both. See Line annotations for remapping rules, stable metadata IDs, and annotation-content lifetime guidance.

Usage Notes

  • In React, pass items for controlled item ownership.
  • In React, pass initialItems instead of items for imperative item ownership. initialItems seeds the viewer once; later item changes should go through the ref.
  • In React, addItems, removeItem, and updateItem require imperative item ownership and throw if the viewer is controlled with items.
  • In React, use selectedLines and onSelectedLinesChange when selection needs to live in component state.
  • In React, use the ref for scrollTo, setSelectedLines, getSelectedLines, clearSelectedLines, getItem, updateItem, addItems, removeItem, and getInstance.
  • renderCustomHeader, renderHeaderPrefix, renderHeaderFilenameSuffix, renderHeaderMetadata, renderAnnotation, and renderGutterUtility receive the whole CodeViewItem, which makes it easy to branch on item.type.
  • In Vanilla JS, CodeView owns a scrollable root that you set up once and update over time.
  • In Vanilla JS, call setup(root) once with the scrollable container.
  • In Vanilla JS, use setItems, addItem, or addItems to populate the viewer, and getItem, removeItem, or updateItem for item-level imperative changes.
  • Shared callbacks receive the normal file/diff payload plus a context argument containing the current viewer item and instance.
  • onPostRender receives (node, instance, phase, context). Its unmount phase will fire when an item scrolls out of the rendered window and CodeView recycles that item's DOM shell.
  • By default, CodeView temporarily disables pointer events on rendered content while scrolling for smoother scroll performance. Set pointerEventsOnScroll: true only when pointer interactions must remain active during scroll.
  • In Vanilla JS, call cleanUp() when the viewer is removed so observers, timers, and DOM state are released.

Scroll Targets

scrollTo supports four target shapes:

Line, range, and item targets resolve against live measured layout, so they continue to work even when wrapped lines or annotations change the rendered heights after initial paint.

Relationship To Virtualization

If your scrollable region is only code, CodeView should usually be your starting point. It is heavily optimized for that case: it owns the whole code region, only renders what is visible, and is generally more performant and less prone to blanking than the lower-level virtualization APIs.

Drop down to Virtualization when you need a more flexible, mixed-content layout that CodeView cannot own directly. That flexibility comes with trade-offs: the lower-level virtualizer always mounts every top-level file or diff container, can blank more easily during aggressive scroll, and is generally less performant than CodeView.

Edit modeBeta

Edit mode is experimental and subject to change.

Edit mode adds text editing, multiple selections, undo and redo, search and replace, markers, and custom selection actions to Pierre file and diff components. The existing component continues to own syntax highlighting, diff layout, annotations, SSR markup, and virtualization.

Use it when a user needs to review and correct rendered code without replacing the surrounding file or diff UI with a separate editor.

For the complete lifecycle, see Handle changes and completion. The later sections cover history, focus, and view state, annotations and custom editing UI, and performance and loading. For component-specific integration details, see React API, Vanilla JS API, and CodeView editing.

Enable editing

An Editor attaches to one rendered File, VirtualizedFile, FileDiff, or VirtualizedFileDiff. React components and CodeView create and attach editors for you; standalone vanilla components use editor.edit(instance) directly.

Each editor is dedicated to either 'file' or 'file-diff' and attaches to one matching component at a time.

React

Mount one stable EditProvider, forward all three factory arguments, and set edit on a File, FileDiff, MultiFileDiff, or PatchDiff to begin editing.

Pass onEditChange to monitor live changes while in edit mode. Use onEditComplete to accept or reject content changes when the edit session ends. See Handle changes and completion for their lifecycle and event payloads.

createEditor, editorOptions, editStateKey, and ownsVerticalViewport are creation-time inputs for an editor. Changing them does not replace an active editor; the latest values apply when a later edit session creates another editor. Keep factories and object props stable across renders.

Vanilla JS

Render the component first, then attach an editor of the matching kind. Keep the function returned by edit() and call it for a normal, install-capable completion.

Set onEditChange in the component's FileOptions or FileDiffOptions to monitor live changes while in edit mode. Use onEditComplete to accept or reject content changes when the edit session ends. See Handle changes and completion for their lifecycle and event payloads.

editor.cleanUp('discard') also runs completion for changed content, but never installs the callback result. cleanUp('recycle') temporarily detaches rendering without ending the session; virtualized hosts use it when a component leaves the render window.

CodeView

CodeView owns one editor per editable item. React gets the factory from the nearest EditProvider; vanilla CodeView takes createEditor in its options. Set edit: true on an item and increment its version whenever edit changes.

Use onItemEditChange(event, item) to monitor an item's live changes while in edit mode. Use onItemEditComplete(event, item, nextItem) to accept or reject content changes when its edit session ends. nextItem is the completed replacement. In controlled React mode, write it into your items state before returning 'accept'. A collapsed or virtualized item keeps its session active; turning editing off, removing the item, resetting, cleaning up, or unmounting ends it. See Handle changes and completion for the shared lifecycle and event payloads.

Use getEditStateKey(item) for per-item edit state retention. It is evaluated only when that item's editor is created. Item editor state retains selections and horizontal code scroll, but they never own or restore the shared CodeView scroll position.

See CodeView editing for item ownership, removal, and controlled-state details.

Handle changes and completion

Use the callbacks exposed by each component:

  • React File, FileDiff, MultiFileDiff, and PatchDiff expose onEditChange and onEditComplete as component props.
  • Vanilla File and FileDiff expose the same callbacks in FileOptions and FileDiffOptions.
  • React and vanilla CodeView use onItemEditChange and onItemEditComplete so each callback also receives the owning item.

onEditChange(event) is the live document change stream. Its EditorChangeEvent contains:

  • changes contains normalized text edits with zero-based ranges and offsets.
  • file is the current editable document, or the editable new side of a diff.
  • lineAnnotations is the current remapped annotation collection when present.
  • editor is the attached editor after the change. Pull state with event.editor.getViewState() or event.editor.getEditState() only when needed.

Do not feed event.file or event.lineAnnotations back into the active component on every change. Edit mode already owns the live document and annotation positions. Use the event for autosave, validation, or application side effects.

Most of the time you probably won't need this API.

Completion runs only when the final contents differ from the latest external input. Selection-only, scroll-only, annotation-only, and fully undone sessions do not call onEditComplete. For React components and CodeView items, completion is triggered when edit changes from true to false or the component or item unmounts. Direct Editor usage ends the session through the function returned by edit(). You must return 'accept' or 'reject' string literals to instruct the component what to do next.

The frozen completion event contains the detached result and the latest external values a rejection restores:

  • file / fileDiff is the completed result.
  • originalFile / originalFileDiff is the latest caller-provided value.
  • Diff events also include complete oldFile and newFile values.
  • lineAnnotations is the final collection; originalLineAnnotations is the latest caller-provided collection.
  • editor is detached, but final getViewState() and getEditState() remain available during the callback.

Return 'accept' to install the completed value while the component remains mounted, or 'reject' to restore the latest external value. A missing handler rejects automatically. Store an accepted value in application state so a later render does not replace it with the original stale file or fileDiff.

For CodeView, onItemEditComplete(event, item, nextItem) receives the completed replacement item. Returning 'accept' installs nextItem; a controlled React owner must also write that item into its items state. Returning 'reject' restores the latest external item.

Unmounting a changed component or item can still run completion, but does not install the result. The callback remains the place to capture or persist that final content.

Accept and reject control what the component displays. They do not clear a keyed editing session.

Retain edit state in memory

By default during any edit session, the Editor will manage editor state for you, including undo/redo history. However, once the edit session has ended, that session data will be thrown away unless you provide an editStateKey to your Editor instantiation.

Future edit sessions with the same editStateKey can resume that undo history and other editor state, including selections and cursor position.

For React components, simply pass the key as props:

<File file={file} edit={editing} editStateKey={`review:${file.name}`} />

For VanillaJS same key can be passed as the third argument to new Editor() or returned by the CodeView getEditStateKey option or prop. A key retains:

  • Current document contents, name, language, and undo/redo history
  • Cursor and selections
  • Surface-local horizontal scrollLeft
  • Vertical scrollTop only for an explicitly owned viewport
  • The old-side baseline and hunk metadata needed to resume a FileDiff safely

Retention is in memory, bounded by an LRU, and does not survive page reload. File and file-diff keys use independent namespaces. The same kind and key cannot be active in two editors at once. A retained FileDiff can resume only against a compatible old-side baseline.

Changing a key while an editor is active does not re-key that editor. The new key applies when a later editor is created.

Clear retained EditState

EditStateManager clears inactive retained sessions. Clearing an active or missing key returns false and does not alter the live editor.

import { EditStateManager } from '@pierre/diffs/edit';

const editStateKey = 'review:example.ts';

// Remove the complete retained session.
EditStateManager.clear('file', editStateKey);

// Keep the draft while selectively resetting retained state.
EditStateManager.clear('file', editStateKey, { history: true });
EditStateManager.clear('file', editStateKey, { selections: true });
EditStateManager.clear('file', editStateKey, { view: true });
EditStateManager.clear('file', editStateKey, { editor: true });

// Remove every inactive retained file and file-diff session.
EditStateManager.clearAll();

// Retain up to 50 inactive entries in each namespace.
EditStateManager.setCapacity(50);

{ document: true } removes the complete entry because history and editor state depend on that document. { editor: true } clears both selections and view state. setCapacity(capacity) changes the maximum number of inactive entries retained by each namespace; the default is 100 per namespace.

Initialize state on first attach

Use initialState to provide application-owned state when constructing an editor. documentKind is required and must match the editor kind. Every other field is optional:

  • document supplies the live TextDocument, including its current contents and any undo/redo history.
  • fileInfo supplies the file name and optional language override.
  • editor supplies selections and editor-owned scroll state.
  • diffSession supplies the old-side baseline and hunk metadata for a file-diff editor.

The editor adopts initialState, it doesn't clone it. On its first attachment, it fills omitted fields from the attached component and defaults. initialState is constructor-only and is consumed by that first attachment.

For a file-diff editor, view-only initialization can omit the document and diff-session fields. Transferring an edited document and its undo/redo history requires the matching document, fileInfo, and diffSession from the same complete EditState; diff-session metadata is not standalone state.

Persist a draft across reloads

editStateKey retains an editing session only in memory. To continue editing in a new page session, persist the latest FileContents and, optionally, its EditorViewState. Rebuild those values as initialState when editing begins again. See Initialize state on first attach and View state and viewport ownership for the corresponding APIs.

  • The latest FileContents. Save event.file from onEditChange to recover an in-progress draft, and save the accepted event.file from onEditComplete as the application's final file state.
  • The optional EditorViewState returned by getViewState(). It contains copied selections and scroll positions and is safe to encode as JSON.

On the next page load, construct a fresh TextDocument from the persisted file and supply it through initialState with the file identity and optional EditorViewState. Pass the same file to the component. The new document restores the latest contents with fresh undo/redo history; the editor builds any omitted state from the attached component.

onEditChange runs for document changes, not selection or scroll movement. The example therefore also captures the current file and EditorViewState at an explicit save point. Applications can do the same before navigation or at another application-owned checkpoint.

EditorViewState is different from the complete EditState returned by getEditState(). Complete state includes the live document and undo/redo history, is borrowed by reference, and can contain non-JSON annotation values, functions, DOM nodes, and cycles. It is not a serialization contract.

Pierre does not currently expose serializable undo/redo history. Restoring a persisted file and EditorViewState starts a new history: edits made before the reload cannot be undone in the new page session.

Control the editor

Use onAttach to capture an editor instance for toolbars and to opt into initial focus. At this point the document and editable DOM are ready.

Vanilla CodeView can add the same behavior in its factory:

'first-visible' targets the first editable row whose top is inside the usable viewport. offset adds a non-negative CSS-pixel inset and preventScroll: true preserves vertical position. Numeric line numbers are one-based; character offsets are zero-based.

Undo and redo

Typed input and applyEdits() share one timeline. undo() and redo() restore the associated selections; canUndo and canRedo drive toolbar state. historyMaxEntries defaults to 100.

View state and viewport ownership

getViewState() returns copied selections and restorable component-owned scroll offsets. An attached component reports horizontal scrollLeft. It reports vertical scrollTop only when the editor was constructed with ownsVerticalViewport: true and the component exposes an exclusive element viewport.

setViewState() requires an attached editor. It restores selections and horizontal scroll; scrollTop is applied only to an owned viewport.

ownsVerticalViewport defaults to false and is captured by the constructor. Enable it only when one editable component exclusively owns the vertical scroller. Virtualization does not imply ownership, and later setOptions() calls cannot change it. Never enable it for individual CodeView items because they share one viewer viewport.

The retained view-state checkpoint updates after synchronization, edits, explicit setViewState() calls, recycling, and completion. Pure selection or scroll movement does not update that checkpoint; call getViewState() for the exact live position.

Extend the editing UI

Selection Action

Set enabledSelectionAction: true and return custom UI from renderSelectionAction to show a popover beside a user-created ranged selection. Programmatic setSelections() and setViewState() update the selection without opening it.

The render context provides the active selection, live document, helpers to read or replace selected text, applyEdits, and close:

Markers

Markers add inline diagnostics with severity styling and hover messages. Call setMarkers() after attachment; pass an empty array to clear them. Marker positions re-anchor as the document changes.

Line annotations

Edit mode owns annotation positions during a session. Additions-side and file annotations follow structural edits; deletion-side annotations and file-level lineNumber: 0 annotations stay fixed. Undo and redo restore annotations with their document state.

Do not mirror remapped event.lineAnnotations back on every change. A genuinely new caller-provided annotation array replaces the edit owned collection and may trigger unintended positional reverts. Reject restores the latest external collection; accept installs the final collection, which should be committed alongside the accepted file or diff.

For interactive annotation content, keep application state under a stable, position-independent metadata ID. React may recreate annotation content when rows move, disappear, or leave a virtualized window.

Performance and loading

Worker Pool

The edited file renders on the main thread while a worker pool continues to render other components. Setting useTokenTransformer: true in the pool's highlighter options can avoid a one-time re-render when entering edit mode, at the cost of larger markup for every pool-rendered file.

See Worker Pool for setup details.

Lazy import

@pierre/diffs/edit is a separate entry point, so it can be loaded only when the feature is used.

API reference

Editor options

Pass options to new Editor(documentKind, options, editStateKey?). initialState and ownsVerticalViewport are constructor-only. setOptions() merges later values, but not every option is retroactive: for example, matchBrackets is captured when the document synchronizes, and changing historyMaxEntries does not resize history already created for a document. Callbacks that are read when an event occurs use their latest values.

EditorKeymap is an array of { platform?, bindings } groups. Platform values are mac, windows, or linux; later groups win when bindings overlap. Custom bindings take precedence and unmatched shortcuts fall back to the built-in keymap.

Keyboard shortcuts

Shortcuts use Cmd on macOS and Ctrl on Windows and Linux. Jumping to the document start or end uses that modifier with Home and

End; on macOS, the modifier with the up and down arrows works too.

33 of 33 bindings
Default editor keyboard shortcuts
ShortcutCommandAction
All platforms · 22 bindings
TabindentIndent line or selection
ShiftTaboutdentOutdent line or selection
Cmd/Ctrl[indentLessDecrease indentation
Cmd/Ctrl]indentMoreIncrease indentation
Cmd/CtrlZundoUndo
Cmd/CtrlShiftZredoRedo
Cmd/CtrlAselectAllSelect all
Cmd/CtrlDfindNextMatchFind next match of the selection
Cmd/CtrlFopenSearchPanelOpen search
Cmd/CtrlAltFopenSearchReplacePanelOpen search and replace
AltmoveLineUpMove selected line(s) up
AltmoveLineDownMove selected line(s) down
ShiftAltcopyLineUpCopy selected line(s) up
ShiftAltcopyLineDownCopy selected line(s) down
EscsimplifySelectionCollapse to a single cursor
Cmd/CtrlEnterinsertBlankLineInsert a blank line
Cmd/Ctrl/toggleCommentToggle line comment
ShiftAltAtoggleBlockCommentToggle block comment
Cmd/CtrlHomemoveCursorToDocStartMove cursor to document start
Cmd/CtrlEndmoveCursorToDocEndMove cursor to document end
Cmd/CtrlShiftHomeexpandSelectionDocStartExtend selection to document start
Cmd/CtrlShiftEndexpandSelectionDocEndExtend selection to document end
macOS · 7 bindings
CtrlKdeleteHardLineForwardDelete to the end of the line
CtrlAltPmoveLineUpMove selected line(s) up
CtrlAltNmoveLineDownMove selected line(s) down
CmdmoveCursorToDocStartMove cursor to document start
CmdmoveCursorToDocEndMove cursor to document end
CmdShiftexpandSelectionDocStartExtend selection to document start
CmdShiftexpandSelectionDocEndExtend selection to document end
Linux · 3 bindings
CtrlYredoRedo
CtrlAltPmoveLineUpMove selected line(s) up
CtrlAltNmoveLineDownMove selected line(s) down
Windows · 1 binding
CtrlYredoRedo

Virtualization

Virtualization in Diffs uses estimated line and file heights to keep large renders fast. It renders placeholders and spacer buffers for off-screen content, then renders visible lines in hunk-sized batches as you scroll.

If your scrollable region is only code, start with CodeView instead. It is the more optimized path: it owns the entire code region, only renders what you can actually see, and is generally more performant and less prone to blanking.

Use the lower-level virtualization APIs when you need a more flexible, mixed-content layout where the code has to live alongside other DOM that is harder for CodeView to control. That flexibility comes with tradeoffs: every top-level file or diff container stays mounted, and the experience is more likely to blank during fast scroll.

Internally, the virtualizer listens to scroll and resize updates, computes a window with overscan, and reconciles measured DOM heights after render. This keeps scroll position stable even when line heights change because of wrapped lines or annotations.

For best results, you'll need to pass a metrics config object to your files or diffs when your layout differs from the defaults. These metrics help the Virtualizer estimate file and diff sizes more accurately before content is measured. For large diffs, using virtualization with a Worker Pool is strongly recommended.

Getting Started

To use virtualization, start with a scrollable container (an HTML element or the window). Directly inside that container, add a content wrapper that holds all diff/file instances and any other content you render. The virtualizer uses this wrapper to track content size changes.

Inside that scroll container, render the VirtualizedFile and VirtualizedFileDiff components. In React, this is handled automatically by the built-in Virtualizer context. In vanilla JS, you manage this explicitly by creating a Virtualizer instance and wiring it to VirtualizedFile / VirtualizedFileDiff instead of the traditional APIs.

React

In React, wrap your diff/file components in Virtualizer from @pierre/diffs/react. The Virtualizer component is your scroll container. Currently, the React wrapper does not support window scrolling unless you orchestrate your own provider via VirtualizerContext.Provider (from @pierre/diffs/react) and pass a manually created Virtualizer instance (from @pierre/diffs).

You can tune virtualization behavior with the config prop.

Virtualizer props:

  • config: partial virtualizer config (overscrollSize, intersectionObserverMargin, resizeDebugging)
  • className / style: applied to the outer scroll root
  • contentClassName / contentStyle: applied to the inner content wrapper

Vanilla JS

In vanilla JS, create a Virtualizer instance and pass it into VirtualizedFileDiff or VirtualizedFile.

Notes

  • Prefer virtualization for very large files or long diff lists any sort of scenario where it's hard to anticipate the constraints of the of the scroll view
  • Keep metrics aligned with your layout if you customize heights.
  • Use resizeDebugging with the Virtualizer temporarily when tuning metrics, and to confirm everything is working properly. Don't forget to disable it in production.
  • While in edit mode, annotations (both their visibility and positions) will be managed for you. Do not feed the onChange results back into component state. See Line annotations.

Hunk Separators

The hunkSeparators option controls how collapsed unchanged regions are displayed. For customization, we recommend starting with a built-in preset and layering unsafeCSS on top.

Passing a render function is only documented for the Vanilla JS APIs. It is being phased out, does not work well with the container-managed and virtualization-oriented React APIs, and is not compatible with SSR. We strongly recommend avoiding that path and customizing built-in separators with unsafeCSS instead.

The Custom CSS example below keeps the built-in line-info-basic markup and tweaks it with CSS.

  • blends the separator row with the diff background
  • hunk content and controls are rendered in every gutter and content region, but the custom CSS targets only the left-most gutter elements
  • aligns the arrow glyphs with the number column
  • replaces the built-in SVG icon with CSS-generated arrows
  • renders the Expand All button, which is normally hidden by default

Built-in Types

  • line-info: Rounded corner separator with collapsed line count and expansion controls.
  • line-info-basic: Compact, full-width variant of line-info with expansion controls.
  • metadata: Patch-style separator (@@ -x,y +a,b @@) with no expansion controls.
  • simple: Minimal separator bar.

Custom CSS Example

If CSS hooks are not enough, the low-level hunkSeparators(hunkData, instance) function still exists on the Vanilla JS FileDiff API. We only recommend that escape hatch as a last resort. It is being phased out, and it does not fit the container-managed and virtualization-oriented APIs that the React components rely on.

Utilities

Import utility functions from @pierre/diffs. These can be used with any framework or rendering approach.

Annotation type guards

Use isDiffAnnotation and isFileAnnotation to tell diff annotations from file annotations. Use isDiffAnnotationCollection and isFileAnnotationCollection for arrays.

Both collection helpers return true for an empty array. Use the current file or diff context if that distinction matters.

diffAcceptRejectHunk

Programmatically accept or reject individual hunks (or specific change blocks inside a hunk) in a diff. This is useful for building interactive code review interfaces, AI-assisted coding tools, or any workflow where users need to selectively apply changes.

To resolve an entire hunk, pass 'accept', 'reject', or 'both'. To resolve only one change block in a hunk, pass an object with type and changeIndex (for example: diffAcceptRejectHunk(diff, hunkIndex, { type: 'accept', changeIndex: 0 })). changeIndex maps to the target entry in that hunk's hunkContent array.

When you accept a hunk, the new (additions) version is kept and the hunk is converted to context lines. When you reject a hunk, the old (deletions) version is restored. You can also use both as a lower-level way to mux the two sides together, which keeps the old lines first and then appends the new lines before collapsing the result back to context. The function returns a new FileDiffMetadata object with all line numbers properly adjusted for subsequent hunks.

resolveMergeConflict

Apply a merge conflict action payload to a file string and return the next contents.

Experimental: UnresolvedFile-related merge conflict APIs are currently beta/experimental and may change in future releases.

Default merge-conflict buttons work even without callbacks: UnresolvedFile applies the resolution internally.

In vanilla, provide onMergeConflictAction for controlled state (for example, to persist resolved contents, sync external stores, or trigger side effects). Use onMergeConflictResolve when you want uncontrolled resolution plus a notification with the resolved file. React UnresolvedFile is intentionally uncontrolled.

disposeHighlighter

Dispose the shared Shiki highlighter instance to free memory. Useful when cleaning up resources in single-page applications.

getSharedHighlighter

Get direct access to the shared Shiki highlighter instance used internally by all components. Useful for custom highlighting operations.

parseDiffFromFile

Compare file contents and generate a FileDiffMetadata structure. Use this when you have full file contents rather than a patch string. Pass both oldFile and newFile for changed files, oldFile: null for added files, or newFile: null for deleted files.

null means that file side intentionally does not exist. Empty files should use contents: '', not null. Passing null for both sides throws because there is no file to diff.

If both oldFile and newFile have a cacheKey, the resulting FileDiffMetadata will automatically receive a combined cache key (format: oldKey:newKey). See Render Cache for more information.

An optional throwOnError parameter (default: false) controls error handling. When true, parsing errors throw exceptions; when false, errors are logged to the console and parsing continues on a best-effort basis.

parsePatchFiles

Parse unified diff / patch file content into structured data. Handles both single patches and multi-commit patch files (like those from GitHub pull request .patch URLs). An optional second parameter cacheKeyPrefix can be provided to generate cache keys for each file in the patch (format: prefix-patchIndex-fileIndex), enabling caching of rendered diff results in the worker pool.

An optional throwOnError parameter (default: false) controls error handling. When true, parsing errors throw exceptions; when false, errors are logged to the console and parsing continues on a best-effort basis.

trimPatchContext

Trim patches with large context windows down to a fixed context window while keeping valid diff headers.

preloadHighlighter

Preload specific themes and languages before rendering to ensure instant highlighting with no async loading delay.

registerCustomTheme

Register a custom Shiki theme for use with any component. The theme name you register must match the name field inside your theme JSON file.

registerCustomLanguage

Register a custom Shiki language loader and optionally map it to file names or extensions. Use this when you're working with languages not bundled by Shiki or want custom highlighting grammars.

setLanguageOverride

Override the syntax highlighting language for a FileContents or FileDiffMetadata object. This is useful when the filename doesn't have an extension or doesn't match the actual language.

Styling

Diff and code components are rendered using shadow DOM APIs, allowing styles to be well-isolated from your page's existing CSS. However, it also means you may have to utilize some custom CSS variables to override default styles. These can be done in your global CSS, as style props on parent components, or on the FileDiff component directly.

Advanced: Unsafe CSS

For advanced customization, you can inject arbitrary CSS into the shadow DOM using the unsafeCSS option. This CSS will be wrapped in an @layer unsafe block, giving it the highest priority in the cascade. Use this sparingly and with caution, as it bypasses the normal style isolation.

We also recommend that any CSS you apply uses simple, direct selectors targeting the existing data attributes. Avoid structural selectors like :first-child, :last-child, :nth-child(), sibling combinators (+ or ~), deeply nested descendant selectors, or bare tag selectors—these are susceptible to breaking in future versions or in edge cases that may be difficult to anticipate.

We cannot currently guarantee backwards compatibility for this feature across any future changes to the library, even in patch versions. Please reach out so that we can discuss a more permanent solution for modifying styles.

Themes

Pierre Diffs ships with our custom open source themes, Pierre Light and Pierre Dark. We generate our themes with a custom build process that takes a shared color palette, assigns colors to specific roles for syntax highlighting, and builds JSON files and editor extensions. This makes our themes compatible with Shiki, Visual Studio Code, Cursor, and Zed.

Editor / PlatformSource
Visual Studio CodeVS Code Marketplace
CursorOpen VSX
ZedZed Extensions
ShikiTheme repository

While you can use any Shiki theme with Pierre Diffs by passing the theme name to the theme option, you can also create and register custom themes compatible with Shiki and Visual Studio Code. We recommend using our themes as a starting point for your own custom themes—head to our themes documentation to get started.

Themes documentation

Token Hooks

Token hooks are experimental and subject to change.

Token hooks let you attach callbacks to syntax-highlighted tokens for custom hover UI, and LSP textDocument/hover integrations.

The shared prop tables in the React API and Vanilla JS API sections list the exact option names. This section covers behavior, examples, and performance tradeoffs.

Available on:

  • React: MultiFileDiff, PatchDiff, FileDiff, and File
  • Vanilla JS: FileDiff and File

Shared behavior:

  • onTokenEnter, onTokenLeave, and onTokenClick receive tokenText, lineNumber, lineCharStart, lineCharEnd, and tokenElement. Diff variants also receive side.
  • lineCharStart is zero-based and lineCharEnd is end-exclusive.
  • If both token and line click handlers are attached, both will fire.
  • Whitespace-only tokens are excluded unless enableTokenInteractionsOnWhitespace is true.
  • tokenElement is usually the simplest way to apply temporary hover styles.
  • Attaching any token callback enables token metadata automatically for locally rendered components — the markup the callbacks need to fire.
  • Set useTokenTransformer: true when you want token wrappers or experimental selectors like data-char without token callbacks.
  • Enabling token metadata increases DOM size because more token wrappers and attributes are preserved. On large files or many mounted diffs, this can have a noticeable performance cost.
  • If you are using a Worker Pool, set useTokenTransformer: true on WorkerPoolManager — pool-rendered components follow the pool's options, not component callbacks. Worker pools can move highlighting work off the main thread, but they do not reduce the extra DOM size created by token metadata.
  • SSR preloads honor token callbacks the same way the client does; set useTokenTransformer: true on your preload option configs only when you want token markup without callbacks.
  • Edit mode enables token metadata automatically for the file being edited; nothing needs to be configured.

Worker Pool

This feature is experimental and undergoing active development. There may be bugs and the API is subject to change.

Import worker utilities from @pierre/diffs/worker.

By default, syntax highlighting runs on the main thread using Shiki. If you're rendering large files or many diffs, this can cause a bottleneck on your JavaScript thread resulting in jank or unresponsiveness. To work around this, we've provided some APIs to run all syntax highlighting in worker threads. The main thread will still attempt to render plain text synchronously and then apply the syntax highlighting when we get a response from the worker threads.

Basic usage differs a bit depending on if you're using React or Vanilla JS APIs, so continue reading for more details.

Setup

One unfortunate side effect of using Web Workers is that different bundlers and environments require slightly different approaches to create a Web Worker. You'll need to create a function that spawns a worker that's appropriate for your environment and bundler and then pass that function to our provided APIs.

Lets begin with the workerFactory function. We've provided some examples for common use cases below.

Only the Vite and NextJS examples have been tested by us. Additional examples were generated by AI. If any of them are incorrect, please let us know.

Vite

You may need to explicitly set the worker.format option in your Vite Config to 'es'.

NextJS

Workers only work in client components. Ensure your function has the 'use client' directive if using App Router.

VS Code Webview Extension

VS Code webviews have special security restrictions that require a different approach. You'll need to configure both the extension side (to expose the worker file) and the webview side (to load it via blob URL).

Extension side: Add the worker directory to localResourceRoots in your getWebviewOptions():

Create the worker URI in _getHtmlForWebview(). Note: use worker-portable.js instead of worker.js — the portable version is designed for environments where ES modules aren't supported in web workers.

Pass the URI to the webview via an inline script in your HTML:

Your Content Security Policy must include worker-src and connect-src:

Webview side: Declare the global type for the URI:

Fetch the worker code and create a blob URL:

Create the workerFactory function:

Webpack 5

esbuild

Rollup / Static Files

If your bundler doesn't have special worker support, build and serve the worker file statically:

Vanilla JS (No Bundler)

For projects without a bundler, host the worker file on your server and reference it directly:

Usage

With your workerFactory function created, you can integrate it with our provided APIs. In React, you'll want to pass this workerFactory to a <WorkerPoolContextProvider> so all components can inherit the pool automatically. If you're using the Vanilla JS APIs, we provide a getOrCreateWorkerPoolSingleton helper that ensures a single pool instance that you can then manually pass to all your File/FileDiff instances.

When using the worker pool, the theme, lineDiffType, tokenizeMaxLineLength, and useTokenTransformer render options are controlled by WorkerPoolManager, not individual components. Passing these options into component instances will be ignored.

To change render options after WorkerPoolManager instantiates, call setRenderOptions(). Changing render options will force mounted components to re-render and clear the render cache.

If you need token callbacks or experimental token selectors such as data-char, enable useTokenTransformer: true on the worker pool itself. Worker pools can move highlighting work off the main thread, but they do not reduce the extra DOM size created by token metadata. For token callback behavior and performance tradeoffs, see Token Hooks.

Edit mode needs no pool configuration: a file being edited renders on the main thread with the token metadata the editor needs, while the pool keeps rendering every other file. Enabling useTokenTransformer: true on the pool is an optional optimization for edit-heavy apps — pool markup is then already editor-compatible, so entering edit mode skips a one-time re-render of that file.

If you need to control which Shiki engine is used, set preferredHighlighter when initializing the pool ('shiki-js' by default, 'shiki-wasm' optional).

React

Wrap your component tree with WorkerPoolContextProvider from @pierre/diffs/react. All FileDiff and File components nested within will automatically use the worker pool for syntax highlighting.

The WorkerPoolContextProvider will automatically spin up or shut down the worker pool based on its react lifecycle. If you have multiple context providers, they will all share the same pool, and termination won't occur until all contexts are unmounted.

Workers only work in client components. Ensure your function has the 'use client' directive if using App Router.

To change themes or other render options dynamically, use the useWorkerPool() hook to access the pool manager and call setRenderOptions().

Vanilla JS

Use getOrCreateWorkerPoolSingleton to spin up a singleton worker pool. Then pass that as the second argument to File and/or FileDiff. When you are done with the worker pool, you can use terminateWorkerPoolSingleton to free up resources.

To change themes or other render options dynamically, call setRenderOptions(options) on the pool instance.

Render Cache

This is an experimental feature being validated in production use cases. The API is subject to change.

The worker pool can cache rendered AST results to avoid redundant highlighting work. When a file or diff has a cacheKey, subsequent requests with the same key will return cached results immediately instead of reprocessing through a worker. This works automatically for both React and Vanilla JS APIs.

Caching is enabled per-file/diff by setting a cacheKey property. Files and diffs without a cacheKey will not be cached. The cache also validates against render options — if options like theme or line diff type change, the cached result is skipped and re-rendered.

API Reference

These methods are exposed for advanced use cases. In most scenarios, you should use the WorkerPoolContextProvider for React or pass the pool instance via the workerPool option for Vanilla JS rather than calling these methods directly.

Architecture

The worker pool manages a configurable number of worker threads that each initialize their own Shiki highlighter instance. Tasks are distributed across available workers, with queuing when all workers are busy.

SSR

Import SSR utilities from @pierre/diffs/ssr.

The SSR API allows you to pre-render file diffs on the server with syntax highlighting, then hydrate them on the client for full interactivity.

If you pass prerenderedHTML, onPostRender still fires on the client after hydration with phase: 'mount'. On later renders, it also fires after DOM-committing updates with phase: 'update', whether those updates are a full replacement or a partial update. Before a mounted container is removed, replaced, or recycled, it fires with phase: 'unmount'. This is useful for measuring, observing, cleaning up, or otherwise manipulating the mounted diff container or content of the diffs itself.

Usage

Each preload function returns an object containing the original inputs plus a prerenderedHTML string. This object can be spread directly into the corresponding React component for automatic hydration.

Inputs used for pre-rendering must exactly match what's rendered in the client component. We recommend spreading the entire result object into your File or Diff component to ensure the client receives the same inputs that were used to generate the pre-rendered HTML.

Server Component

Client Component

Preloaders

We provide several preload functions to handle different input formats. Choose the one that matches your data source.

preloadFile

Preloads a single file with syntax highlighting (no diff). Use this when you want to render a file without any diff context. Spread into the File component.

preloadUnresolvedFile

Preloads a merge-conflict file for UnresolvedFile hydration. Use this when the file contains conflict markers (<<<<<<<, =======, >>>>>>>) and you want to preserve the unresolved conflict UI from SSR to client.

Experimental: UnresolvedFile and preloadUnresolvedFile are currently beta/experimental and may change in future releases.

preloadFileDiff

Preloads a diff from a FileDiffMetadata object. Use this when you already have parsed diff metadata (e.g., from parseDiffFromFile or parsePatchFiles). Spread into the FileDiff component.

The lower-level preloadDiffHTML helper also accepts direct file contents. Use oldFile: null for a new file, or newFile: null for a deleted file.

preloadMultiFileDiff

Preloads a diff directly from file contents. This is the simplest option when you have raw file contents and want to generate a diff. Pass both sides for a changed file, oldFile: null for a new file, or newFile: null for a deleted file. Spread into the MultiFileDiff component.

preloadPatchDiff

Preloads a diff from a unified patch string for a single file. Use this when you have a patch in unified diff format. Spread into the PatchDiff component.

preloadPatchFile

Preloads multiple diffs from a multi-file patch string. Returns an array of results, one for each file in the patch. Each result can be spread into a FileDiff component.