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 surfaces
@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

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 read-only 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. When Editor.persistState is enabled, every editable file requires an explicit, non-empty cacheKey. Use unique, stable keys for editing sessions, and change the key when incoming contents should replace the cached document.

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 surfaces 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 Editing with 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 surfaces use edit and editorOptions; CodeView uses item edit flags and its own editorOptions. Each active surface or item receives an independent editor. UnresolvedFile is not editable. See Edit mode → React and CodeView → Editing for lifecycle and callback details.

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).

When one of these components is attached to an Editor, keep its annotations in application-owned state and replace them with the current collection emitted by Editor.onChange. See Editing with line annotations for the React flushSync pattern and annotation-content lifetime guidance.

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().

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).

When one of these components is attached to an Editor, keep its annotations in an external variable or store and replace them with the current collection emitted by Editor.onChange. See Editing with line annotations for the synchronization pattern and remapping rules.

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. See CodeView for the dedicated guide.

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 reports live contents with the owning item. If a session produces a change, onItemEditComplete reports its latest contents when editing is disabled or the item is collapsed or removed. Direct reset, cleanup, and viewer unmount are silent.

Each edited item receives an independent editor whose history survives virtualization. Changes to the provider factory or editorOptions do not disturb active sessions; their latest values apply the next time an item enters edit mode.

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({
  createEditor(options) {
    return new Editor({
      ...options,
      onAttach(editor) {
        editor.focus({ lineNumber: 'first-visible', preventScroll: true });
      },
    });
  },
});

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 Autofocus on Attach 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 surfaces 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

When an editable item has annotations, onItemEditChange receives the owning item, its edited FileContents, and the complete current annotation collection. For a diff item, those contents represent the editable new-file side. When the emitted annotation array is a different object, replace item.annotations and increment the item's version. CodeView does not write either value for you.

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.

When React owns the CodeView item list through items, publish a new items array containing the updated item inside flushSync so annotation placement updates with the edited content before paint. When CodeView owns the list in React's imperative mode, call updateItem through the component ref; in vanilla JS, call updateItem on the CodeView instance. Skip the update when the emitted array is the same object as the item's current annotations, since ordinary same-line typing reuses that array.

Keep onItemEditComplete focused on committing the final file contents or rebuilding fileDiff with a fresh cacheKey. Live annotations should already be synchronized through onItemEditChange. See Editing with 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 obs