npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2026 – Pkg Stats / Ryan Hefner

vue-diff-next

v1.2.0

Published

A modern, lightweight diff viewer component for Vue 3.5+

Readme

Release GitHub License CI NPM Downloads Sponsor

vue-diff-next

vue-diff-next screenshot, split mode

A lightweight diff viewer component for Vue 3.5+. Inspired by vue-diff, which was archived in February 2025.

Same props, same modes, same look. ~18 kB gzip for the default bundle (style.css + component JS) with 30 languages bundled, versus roughly 75 kB for the original covering 7.

Try the Live Demo to see it in action.

  • Drop-in replacement — every vue-diff prop and default is preserved, verified against the original's own output
  • 30 languages, all bundled; no per-language imports or registration
  • Split and unified modes, with word-level highlighting composed on top of syntax highlighting

Install

npm install vue-diff-next

Vue 3.5 or newer is a peer dependency.

Usage

As a component:

<script setup>
import { Diff } from 'vue-diff-next';
import 'vue-diff-next/style.css';
</script>

<template>
  <Diff mode="split" theme="dark" language="javascript" :prev="before" :current="after" />
</template>

Or registered globally as a plugin:

import { createApp } from 'vue';
import VueDiff from 'vue-diff-next';
import 'vue-diff-next/style.css';

createApp(App).use(VueDiff).mount('#app');
// Renders as <Diff>, or pass { componentName: 'VueDiff' } to rename it.

The stylesheet is not optional — the component ships unstyled without it.

Props

| Prop | Type | Default | Description | | ----------------- | --------------------------------------------- | ------------- | ------------------------------------------------------------------------ | | mode | 'split' \| 'unified' | 'split' | Side-by-side or interleaved. | | theme | 'dark' \| 'light' \| custom* \| ... | 'dark' | See Theming. | | language | string | 'plaintext' | See Languages. | | prev | string \| null | '' | The "before" text. | | current | string \| null | '' | The "after" text. | | showLineNumbers | boolean | true | Diff line-number gutter. Set false to hide it. | | folding | boolean | false | Collapse long runs of unchanged lines. See Folding. | | foldMarker | 'dots' \| 'hunk' | 'dots' | How a collapsed run is marked. See Folding. | | inputDelay | number | 0 | Debounce re-rendering, in ms. Useful for editor-driven input. | | virtualScroll | boolean \| { height, lineMinHeight, delay } | false | Render only the rows near the viewport. See Large diffs. | | wrap | boolean | true | Soft-wrap long lines. Set false to scroll horizontally instead. |

Migrating from vue-diff

Change the import and the stylesheet path:

- import VueDiff from 'vue-diff';
- import 'vue-diff/dist/index.css';
+ import VueDiff from 'vue-diff-next';
+ import 'vue-diff-next/style.css';

That is the whole migration. Props, defaults, modes, and both install paths are unchanged.

A few things to know:

  • theme="custom*" still works, but the CSS custom properties you override are named differently. See Theming.
  • theme="light" / "dark" are a different palette than vue-diff's highlight.js vs / monokai. For those colors use visual-studio-light / monokai-dark and import the matching extra stylesheet (see Theming).
  • Language names still work, and more are accepted. javascript, plaintext, markdown, and typescript all resolve as before.
  • virtualScroll behaves as it did, windowing the output to what is near the viewport. See Large diffs.

How it differs internally

Same behaviour, different internals:

  • diff (jsdiff) instead of diff-match-patch for line and word diffing.
  • @speed-highlight/core instead of highlight.js. The reduced library size is mostly the highlighter: highlight.js's engine alone is 22.4 kB gzip, while @speed-highlight/core's engine plus all bundled grammars is about 9 kB.
  • No @vueuse/core. Its debounce was the only part used; that is now a few lines.
  • Word-diff and syntax highlighting are composed at the token level. The original injected <vue-diff-modified> marker strings into the source before highlighting and string-replaced them afterwards, which breaks if the content contains the marker. Here the two overlapping classifications are merged by splitting the token stream at word boundaries, so no string injection happens and content cannot impersonate a marker.
  • Highlighting is synchronous. Grammars ship in the bundle, so each visible row tokenizes in the same tick as the source change.

Folding

With folding, runs of unchanged lines collapse to a single marker row, so a small change in a large file doesn't bury the diff. foldMarker chooses how that row looks:

<Diff :folding="true" fold-marker="hunk" :prev="before" :current="after" />

dots (default) — a centred •••••, matching the original:

  12  const config = {
   >  • • • • •
  47    timeout: 5000,

hunk — a unified-diff header, stating how much was skipped rather than only that something was:

  12  const config = {
   >  @@ -13,34 +13,34 @@
  47    timeout: 5000,

Both are styleable — see --vue-diff-fold-* under Theming. The dots' size and spacing are variables, so you can tune them without overriding the content.

A changed line is never hidden by folding, and the first unchanged line after each change stays visible as context.

Languages

Pass any of these to language. Aliases in the right column resolve to the same grammar, so highlight.js names and file extensions both work:

| Group | Languages | Also accepted | | ----------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | Web | html css js ts json xml | htm vue · scss sass less · javascript jsx mjs cjs · typescript tsx · svg · jsdoc | | Systems | c rs go java py pl lua asm bf | cpp c++ h cs · rust · golang · python py3 · perl · assembly · brainfuck | | Data & config | yaml toml ini csv sql md leanpub-md | yml · conf cfg · markdown | | Shell & ops | bash docker make git diff http uri log | sh zsh shell · dockerfile · makefile · patch · url | | Other | regex todo plain | plaintext text |

Matching is case-insensitive and surrounding whitespace is ignored.

An unknown language renders as plain text rather than throwing, so user-supplied language names are safe to pass straight through.

Theming

| theme | Palette | Stylesheet | | --------------------- | ------------------------------------------ | ---------------------------------------------- | | dark (default) | VS Code-ish dark | vue-diff-next/style.css | | light | VS Code-ish light | vue-diff-next/style.css | | atom-dark | Atom One Dark | vue-diff-next/themes/atom-dark.css | | atom-light | Atom One Light | vue-diff-next/themes/atom-light.css | | coral-dark | Coral keywords, teal strings | vue-diff-next/themes/coral-dark.css | | coral-light | Same tokens, darkened for a white canvas | vue-diff-next/themes/coral-light.css | | github-dark | GitHub Dark | vue-diff-next/themes/github-dark.css | | github-light | GitHub Light | vue-diff-next/themes/github-light.css | | monokai-dark | highlight.js monokai (vue-diff dark) | vue-diff-next/themes/monokai-dark.css | | twilight-dark | highlight.js base16-twilight | vue-diff-next/themes/twilight-dark.css | | visual-studio-dark | highlight.js vs2015 | vue-diff-next/themes/visual-studio-dark.css | | visual-studio-light | highlight.js vs (vue-diff light) | vue-diff-next/themes/visual-studio-light.css | | custom* | Unstyled; you supply the CSS | none |

dark and light ship in the default stylesheet. Extra palettes are a second import — the wrapper class is always applied; without the extra CSS they look like an unstyled custom* theme:

import 'vue-diff-next/style.css';
import 'vue-diff-next/themes/visual-studio-light.css';
<Diff theme="visual-studio-light" ... />

If you bind theme dynamically, import every extra file you might select.

Everything is driven by CSS custom properties, so you can override any part without forking the stylesheet:

.vue-diff-theme-dark {
  --vue-diff-added-bg: #143d2b;
  --vue-diff-removed-bg: #45161a;
  --vue-diff-syn-kwd: #ff7b72;
}

For a wholly separate theme, pass any theme value beginning with custom. The component adds vue-diff-theme-<value> to its wrapper and ships no styles for it — you supply them:

<Diff theme="custom-solarized" ... />
.vue-diff-theme-custom-solarized {
  --vue-diff-bg: #002b36;
  --vue-diff-fg: #839496;
  /* …and the rest */
}

Available properties: --vue-diff-{bg,fg,font-family,font-size,line-height,gutter-width}, --vue-diff-{gutter-bg,gutter-fg}, --vue-diff-{added,removed,disabled}-bg, --vue-diff-{added,removed}-gutter-bg, --vue-diff-{added,removed}-word-bg, --vue-diff-fold-{bg,fg}, --vue-diff-fold-dot-{size,spacing,opacity}, --vue-diff-fold-hunk-opacity, and --vue-diff-syn-{kwd,str,num,bool,cmnt,func,class,var,type,oper,section,insert,deleted,err}.

Large diffs

A few thousand rows lay out and paint slowly — the cost is DOM size and per-line highlighting, not diffing. Each visible row tokenizes in the same tick; without windowing, a large file means thousands of DOM nodes and thousands of highlight passes.

Use virtualScroll for large diffs. It renders only the rows near the viewport:

<Diff :virtual-scroll="true" :prev="before" :current="after" />
<!-- or tune it -->
<Diff
  :virtual-scroll="{ height: 500, lineMinHeight: 24, delay: 100 }"
  :prev="before"
  :current="after"
/>

| Option | Default | Meaning | | --------------- | ------- | ---------------------------------------------------- | | height | 500 | Viewer height in px. Also sets the windowing extent. | | lineMinHeight | 24 | Assumed row height until one is measured. | | delay | 100 | Scroll throttle in ms. |

The window extends 1.5 viewport heights past each edge, so ordinary scrolling does not outrun it. Row heights are measured rather than assumed — a wrapped line can be any height — so the container's scroll height converges on the truth as rows report in, and the scrollbar reflects the whole diff rather than only what is rendered.

Measured on a 2000-line diff in a 500px viewer: 53 rows in the DOM instead of 2000, with a 48,000px scroll range. Only those visible rows are highlighted.

Security

The component renders untrusted text as HTML, so escaping is a correctness requirement rather than a detail. All text is escaped before insertion, and the escaping boundary is a single small module with its own test suite covering <script> tags, inline event handlers, and attribute-breaking quotes.

Found a hole? Please report it privately — see SECURITY.md.