j3w1 / theme

j3w1 / theme4.0.0

j3w1 / command

  • SPEC Overview
  • SPEC Foundations
  • SPEC Components
  • SPEC Patterns
  • SPEC Tokens
  • SPEC Tools
  • SPEC Workbench
  • SPEC Form builder
  • SPEC Task kit
  • SPEC Recipes
  • SPEC Figma bridge
  • SPEC Report an issue
  • SPEC Implement
  • SPEC For agents
  • SPEC Ports
  • SPEC Releases
  • SPEC Specification
  • DEMO Vue application demo

Diff view

Unified and split diffs as native tables; added, removed and modified rows keep their textual gutter marker, word-level changes are underlines, conflicts are double borders.

stable · light DOM · 4.0.0. Light DOM retains static content, native semantics, label relationships and host composition without duplicating controls.

Live variants

unified

title.js, old against new, unified
Old lineNew lineChangeText
@@ -3,7 +3,8 @@ export function title(container)
33 const name = container.name ?? "untitled";
4− removed let label = name;
4+ added let label = `${name} (${container.children.length})`;
55 if (label.length > MAX_LENGTH) {
66~ modified label = label.slice(0, MAX_LENGTH) + "…";
77 }
Markup
<j3w1-diff-view id="example-diff-view-unified"><div class="diff-view-scroll" tabindex="0" role="region" aria-label="Source differences"><table class="diff-view diff-view-unified">
  <caption class="sr-only">title.js, old against new, unified</caption>
  <thead class="sr-only">
    <tr><th scope="col">Old line</th><th scope="col">New line</th><th scope="col">Change</th><th scope="col">Text</th></tr>
  </thead>
  <tbody>
    <tr class="diff-view-row diff-view-row-hunk">
      <td class="diff-view-ln"></td><td class="diff-view-ln"></td><td class="diff-view-marker"> </td>
      <td class="diff-view-text"><span class="diff-view-range">@@ -3,7 +3,8 @@</span> export function title(container)</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">3</td><td class="diff-view-ln">3</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">  const name = container.name ?? "untitled";</td>
    </tr>
    <tr class="diff-view-row diff-view-row-removed">
      <td class="diff-view-ln">4</td><td class="diff-view-ln"></td><td class="diff-view-marker">−<span class="sr-only"> removed</span></td>
      <td class="diff-view-text">  let label = name;</td>
    </tr>
    <tr class="diff-view-row diff-view-row-added diff-view-row-active">
      <td class="diff-view-ln"></td><td class="diff-view-ln">4</td><td class="diff-view-marker">+<span class="sr-only"> added</span></td>
      <td class="diff-view-text">  let label = `${name} (${container.children.length})`;</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">5</td><td class="diff-view-ln">5</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">  if (label.length &gt; MAX_LENGTH) {</td>
    </tr>
    <tr class="diff-view-row diff-view-row-modified">
      <td class="diff-view-ln">6</td><td class="diff-view-ln">6</td><td class="diff-view-marker">~<span class="sr-only"> modified</span></td>
      <td class="diff-view-text">    label = label.slice(0, MAX_LENGTH) + "…";</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">7</td><td class="diff-view-ln">7</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">  }</td>
    </tr>
    <tr class="diff-view-row diff-view-row-fold">
      <td class="diff-view-fold-cell" colspan="4">
        <button class="diff-view-fold" type="button" aria-expanded="false" aria-controls="example-diff-view-unified-dv-u-fold">
          <svg class="diff-view-chevron" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M6 3l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.5"></path></svg>
          <span class="diff-view-fold-collapsed">… 12 unchanged lines</span><span class="diff-view-fold-expanded">Hide 12 unchanged lines</span>
        </button>
      </td>
    </tr>
  </tbody>
  <tbody class="diff-view-folded" id="example-diff-view-unified-dv-u-fold" hidden="">
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">8</td><td class="diff-view-ln">8</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">  return escape(label);</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">9</td><td class="diff-view-ln">9</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">}</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">10</td><td class="diff-view-ln">10</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">… ten more context lines …</td>
    </tr>
  </tbody>
</table></div></j3w1-diff-view>

split

title.js, old on the left, new on the right
Old lineOld changeOld textNew lineNew changeNew text
@@ -3,7 +3,8 @@ export function title(container)
3 const name = container.name ?? "untitled"; 3 const name = container.name ?? "untitled";
4− removed let label = name; 4+ added let label = `${name} (${container.children.length})`;
5 if (label.length > MAX_LENGTH) { 5 if (label.length > MAX_LENGTH) {
6~ modified label = label.slice(0, MAX_LENGTH); 6~ modified label = label.slice(0, MAX_LENGTH) + "…";
7+ added label = label.trimEnd();
Markup
<j3w1-diff-view id="example-diff-view-split"><div class="diff-view-scroll" tabindex="0" role="region" aria-label="Source differences"><table class="diff-view diff-view-split">
  <caption class="sr-only">title.js, old on the left, new on the right</caption>
  <thead class="sr-only">
    <tr><th scope="col">Old line</th><th scope="col">Old change</th><th scope="col">Old text</th><th scope="col">New line</th><th scope="col">New change</th><th scope="col">New text</th></tr>
  </thead>
  <tbody>
    <tr class="diff-view-row diff-view-row-hunk">
      <td class="diff-view-fold-cell" colspan="6"><span class="diff-view-range">@@ -3,7 +3,8 @@</span> export function title(container)</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln diff-view-side-old">3</td><td class="diff-view-marker diff-view-side-old"> </td><td class="diff-view-text diff-view-side-old">  const name = container.name ?? "untitled";</td>
      <td class="diff-view-ln diff-view-side-new">3</td><td class="diff-view-marker diff-view-side-new"> </td><td class="diff-view-text diff-view-side-new">  const name = container.name ?? "untitled";</td>
    </tr>
    <tr class="diff-view-row diff-view-row-pair diff-view-row-active">
      <td class="diff-view-ln diff-view-side-old diff-view-cell-removed">4</td><td class="diff-view-marker diff-view-side-old diff-view-cell-removed">−<span class="sr-only"> removed</span></td><td class="diff-view-text diff-view-side-old diff-view-cell-removed">  let label = name;</td>
      <td class="diff-view-ln diff-view-side-new diff-view-cell-added">4</td><td class="diff-view-marker diff-view-side-new diff-view-cell-added">+<span class="sr-only"> added</span></td><td class="diff-view-text diff-view-side-new diff-view-cell-added">  let label = `${name} (${container.children.length})`;</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln diff-view-side-old">5</td><td class="diff-view-marker diff-view-side-old"> </td><td class="diff-view-text diff-view-side-old">  if (label.length &gt; MAX_LENGTH) {</td>
      <td class="diff-view-ln diff-view-side-new">5</td><td class="diff-view-marker diff-view-side-new"> </td><td class="diff-view-text diff-view-side-new">  if (label.length &gt; MAX_LENGTH) {</td>
    </tr>
    <tr class="diff-view-row diff-view-row-modified">
      <td class="diff-view-ln diff-view-side-old">6</td><td class="diff-view-marker diff-view-side-old">~<span class="sr-only"> modified</span></td><td class="diff-view-text diff-view-side-old">    label = label.slice(0, MAX_LENGTH);</td>
      <td class="diff-view-ln diff-view-side-new">6</td><td class="diff-view-marker diff-view-side-new">~<span class="sr-only"> modified</span></td><td class="diff-view-text diff-view-side-new">    label = label.slice(0, MAX_LENGTH) + "…";</td>
    </tr>
    <tr class="diff-view-row diff-view-row-added">
      <td class="diff-view-ln diff-view-side-old"></td><td class="diff-view-marker diff-view-side-old"> </td><td class="diff-view-text diff-view-side-old diff-view-cell-blank"></td>
      <td class="diff-view-ln diff-view-side-new">7</td><td class="diff-view-marker diff-view-side-new">+<span class="sr-only"> added</span></td><td class="diff-view-text diff-view-side-new">    label = label.trimEnd();</td>
    </tr>
    <tr class="diff-view-row diff-view-row-fold">
      <td class="diff-view-fold-cell" colspan="6">
        <button class="diff-view-fold" type="button" aria-expanded="false" aria-controls="example-diff-view-split-dv-s-fold">
          <svg class="diff-view-chevron" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M6 3l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.5"></path></svg>
          <span class="diff-view-fold-collapsed">… 12 unchanged lines</span><span class="diff-view-fold-expanded">Hide 12 unchanged lines</span>
        </button>
      </td>
    </tr>
  </tbody>
  <tbody class="diff-view-folded" id="example-diff-view-split-dv-s-fold" hidden="">
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln diff-view-side-old">7</td><td class="diff-view-marker diff-view-side-old"> </td><td class="diff-view-text diff-view-side-old">  }</td>
      <td class="diff-view-ln diff-view-side-new">8</td><td class="diff-view-marker diff-view-side-new"> </td><td class="diff-view-text diff-view-side-new">  }</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln diff-view-side-old">8</td><td class="diff-view-marker diff-view-side-old"> </td><td class="diff-view-text diff-view-side-old">  return escape(label);</td>
      <td class="diff-view-ln diff-view-side-new">9</td><td class="diff-view-marker diff-view-side-new"> </td><td class="diff-view-text diff-view-side-new">  return escape(label);</td>
    </tr>
  </tbody>
</table></div></j3w1-diff-view>

word-level

title.js, unified with word-level changes
Old lineNew lineChangeText
@@ -4,3 +4,3 @@ export function title(container)
4− removed let label = name;
4+ added let label = `${name} (${count})`;
5− removed if (label.length > 48) {
5+ added if (label.length > MAX_LENGTH) {
Markup
<j3w1-diff-view id="example-diff-view-word-level"><div class="diff-view-scroll" tabindex="0" role="region" aria-label="Source differences"><table class="diff-view diff-view-word-level">
  <caption class="sr-only">title.js, unified with word-level changes</caption>
  <thead class="sr-only">
    <tr><th scope="col">Old line</th><th scope="col">New line</th><th scope="col">Change</th><th scope="col">Text</th></tr>
  </thead>
  <tbody>
    <tr class="diff-view-row diff-view-row-hunk">
      <td class="diff-view-ln"></td><td class="diff-view-ln"></td><td class="diff-view-marker"> </td>
      <td class="diff-view-text"><span class="diff-view-range">@@ -4,3 +4,3 @@</span> export function title(container)</td>
    </tr>
    <tr class="diff-view-row diff-view-row-removed">
      <td class="diff-view-ln">4</td><td class="diff-view-ln"></td><td class="diff-view-marker">−<span class="sr-only"> removed</span></td>
      <td class="diff-view-text">  let label = <del class="diff-view-word-removed">name;</del></td>
    </tr>
    <tr class="diff-view-row diff-view-row-added diff-view-row-active">
      <td class="diff-view-ln"></td><td class="diff-view-ln">4</td><td class="diff-view-marker">+<span class="sr-only"> added</span></td>
      <td class="diff-view-text">  let label = <ins class="diff-view-word-added">`${name} (${count})`;</ins></td>
    </tr>
    <tr class="diff-view-row diff-view-row-removed">
      <td class="diff-view-ln">5</td><td class="diff-view-ln"></td><td class="diff-view-marker">−<span class="sr-only"> removed</span></td>
      <td class="diff-view-text">  if (label.length &gt; <del class="diff-view-word-removed">48</del>) {</td>
    </tr>
    <tr class="diff-view-row diff-view-row-added">
      <td class="diff-view-ln"></td><td class="diff-view-ln">5</td><td class="diff-view-marker">+<span class="sr-only"> added</span></td>
      <td class="diff-view-text">  if (label.length &gt; <ins class="diff-view-word-added">MAX_LENGTH</ins>) {</td>
    </tr>
    <tr class="diff-view-row diff-view-row-fold">
      <td class="diff-view-fold-cell" colspan="4">
        <button class="diff-view-fold" type="button" aria-expanded="false" aria-controls="example-diff-view-word-level-dv-w-fold">
          <svg class="diff-view-chevron" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M6 3l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.5"></path></svg>
          <span class="diff-view-fold-collapsed">… 12 unchanged lines</span><span class="diff-view-fold-expanded">Hide 12 unchanged lines</span>
        </button>
      </td>
    </tr>
  </tbody>
  <tbody class="diff-view-folded" id="example-diff-view-word-level-dv-w-fold" hidden="">
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">6</td><td class="diff-view-ln">6</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">    label = label.slice(0, MAX_LENGTH) + "…";</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">7</td><td class="diff-view-ln">7</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">  }</td>
    </tr>
  </tbody>
</table></div></j3w1-diff-view>

conflict

title.js, merge conflict between main and feature/gaps
LineChangeText
3 const name = container.name ?? "untitled";
4 <<<<<<< HEAD (main)
5− ours let label = name;
6 =======
7+ theirs let label = `${name} (${count})`;
8 >>>>>>> feature/gaps
9 if (label.length > MAX_LENGTH) {
Markup
<j3w1-diff-view id="example-diff-view-conflict"><div class="diff-view-scroll" tabindex="0" role="region" aria-label="Source differences"><table class="diff-view diff-view-conflict">
  <caption class="sr-only">title.js, merge conflict between main and feature/gaps</caption>
  <thead class="sr-only">
    <tr><th scope="col">Line</th><th scope="col">Change</th><th scope="col">Text</th></tr>
  </thead>
  <tbody>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">3</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">  const name = container.name ?? "untitled";</td>
    </tr>
    <tr class="diff-view-row diff-view-row-conflict diff-view-row-conflict-start">
      <td class="diff-view-ln">4</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">&lt;&lt;&lt;&lt;&lt;&lt;&lt; HEAD (main)</td>
    </tr>
    <tr class="diff-view-row diff-view-row-conflict diff-view-row-ours diff-view-row-active">
      <td class="diff-view-ln">5</td><td class="diff-view-marker">−<span class="sr-only"> ours</span></td>
      <td class="diff-view-text">  let label = name;</td>
    </tr>
    <tr class="diff-view-row diff-view-row-conflict diff-view-row-conflict-mid">
      <td class="diff-view-ln">6</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">=======</td>
    </tr>
    <tr class="diff-view-row diff-view-row-conflict diff-view-row-theirs">
      <td class="diff-view-ln">7</td><td class="diff-view-marker">+<span class="sr-only"> theirs</span></td>
      <td class="diff-view-text">  let label = `${name} (${count})`;</td>
    </tr>
    <tr class="diff-view-row diff-view-row-conflict diff-view-row-conflict-end">
      <td class="diff-view-ln">8</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">&gt;&gt;&gt;&gt;&gt;&gt;&gt; feature/gaps</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">9</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">  if (label.length &gt; MAX_LENGTH) {</td>
    </tr>
    <tr class="diff-view-row diff-view-row-fold">
      <td class="diff-view-fold-cell" colspan="3">
        <button class="diff-view-fold" type="button" aria-expanded="false" aria-controls="example-diff-view-conflict-dv-c-fold">
          <svg class="diff-view-chevron" width="12" height="12" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M6 3l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.5"></path></svg>
          <span class="diff-view-fold-collapsed">… 12 unchanged lines</span><span class="diff-view-fold-expanded">Hide 12 unchanged lines</span>
        </button>
      </td>
    </tr>
  </tbody>
  <tbody class="diff-view-folded" id="example-diff-view-conflict-dv-c-fold" hidden="">
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">10</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">    label = label.slice(0, MAX_LENGTH) + "…";</td>
    </tr>
    <tr class="diff-view-row diff-view-row-context">
      <td class="diff-view-ln">11</td><td class="diff-view-marker"> </td>
      <td class="diff-view-text">  }</td>
    </tr>
  </tbody>
</table></div></j3w1-diff-view>

Install and register

Use the exact version. If npm publication is pending, install the release tarball from the installation guide.

npm install --save-exact @j3w1/ui@4.0.0
import '@j3w1/ui/tokens.css';
import '@j3w1/ui/styles/diff-view.css';
import '@j3w1/ui/register/diff-view';

Insert the maintained markup above after loading these imports. Its native children provide accessible names and form behavior; the empty custom tag alone does not create a control.

Vue SFC, React, Astro and plain HTML integration →

Copy the complete implementation

npx --package=@j3w1/ui@4.0.0 j3w1-ui copy diff-view --out ./vendor/j3w1/diff-view

The CLI verifies every file before writing and refuses overwrites. Serve the output over HTTP and open index.html. Keep its modules, CSS, tokens and licence notices together.

Public API

attributes

NameType / signatureMeaning
disabledCustomEventDisable the native controls owned by this instance.
loadingCustomEventMark the primary control busy and prevent duplicate activation.

properties

NameType / signatureMeaning
disabledbooleanDelegates disabled state to native children.
namestringNative control name; application form keys remain explicit.

methods

NameType / signatureMeaning
refresh(): voidReconnect behavior after replacing native child structure.
focus(options?: FocusOptions): voidFocus the first usable native control.
setText(text: string): voidReplace the bounded read-only text view; no code or shell execution occurs.

events

NameType / signatureMeaning
j3w1-actionCustomEventRequest a developer-view action; editing and execution belong to the host.

Dependencies and verification

Registered components: none. Style dependencies: none.

Package presence establishes an available implementation. Actual passes depend on the tested artifact and environment. Read specification execution evidence; read packed-consumer evidence; inspect the complete state matrix.

Full implementation manifest, API and token dependency closure

Canonical specification

Diff view diff-view

  • Family: Developer interfaces
  • Maturity: stable
  • Priority: R1
  • Since: 0.1.0
  • Native control: native <table> with a caption; one row per line; collapsed hunks are a <button> with aria-expanded controlling a hidden <tbody> — APG pattern
  • specified
  • demonstrated

Unified and split diffs as native tables; added, removed and modified rows keep their textual gutter marker, word-level changes are underlines, conflicts are double borders.

Report an issue with this component

Tokens

Focus a role to inspect its values; follow its link for persistent usage and source details.

PartRoleValueUse
root.bgcolor.code.bgapproveduse#000000D-041 code.bg: color.primitive.ink.0.
root.bordercolor.border.controlapproveduse#c81a1aD-041 owner correction: red control and floating-frame borders, distinct from rose text.
root.textcolor.text.defaultapproveduse#e99499D-041 owner correction: rose is the default UI and reading foreground; near-white is reserved for document headings, highlights and contrast-critical fills.
line-number.textcolor.text.mutedapproveduse#d4868bD-041 owner correction: rose is the default UI and reading foreground; near-white is reserved for document headings, highlights and contrast-critical fills.
line-number.rulecolor.code.gutter-ruleapproveduse#482b2bD-041 code.gutter-rule: color.primitive.modern.divider.
row.dividercolor.border.dividerapproveduse#482b2bD-041 border.divider: color.primitive.modern.divider.
added.bgcolor.diff.added.bgapproveduse#0f1a0eAdded line background.
added.guttercolor.diff.added.gutterapproveduse#82d9a0D-041 diff.added.gutter: color.primitive.modern.green.
added.emphasiscolor.diff.added.emphasisapproveduse#82d9a0D-041 diff.added.emphasis: color.primitive.modern.green.
removed.bgcolor.diff.removed.bgobserveduse#2b0e0dRemoved line background.
removed.guttercolor.diff.removed.gutterobserveduse#e53935Removed gutter mark and −.
removed.emphasiscolor.diff.removed.emphasisobserveduse#e53935Word-level removal: underline and line-through.
modified.bgcolor.diff.modified.bgapproveduse#1f1a0cModified line background.
modified.guttercolor.diff.modified.gutterapproveduse#e5c07bD-041 diff.modified.gutter: color.primitive.modern.amber.
hunk.bgcolor.diff.header-bgobserveduse#241010Hunk header background.
hunk.textcolor.text.mutedapproveduse#d4868bD-041 owner correction: rose is the default UI and reading foreground; near-white is reserved for document headings, highlights and contrast-critical fills.
hunk.contextcolor.text.defaultapproveduse#e99499D-041 owner correction: rose is the default UI and reading foreground; near-white is reserved for document headings, highlights and contrast-critical fills.
conflict.bordercolor.diff.conflict-borderobserveduse#f73f35Conflict block: double 1px border.
conflict.markercolor.text.brightapproveduse#ffa2a7D-041 owner correction: rose is the default UI and reading foreground; near-white is reserved for document headings, highlights and contrast-critical fills.
fold.textcolor.text.linkapproveduse#f73f35Strong red link text, distinct from rose body text and headings; retain the underline.
fold.bg-hovercolor.interaction.hover.bg-strongapproveduse#1c1010D-041 interaction.hover.bg-strong: color.primitive.modern.hover.
row.bg-hovercolor.interaction.hover.bgapproveduse#1c1010D-041 interaction.hover.bg: color.primitive.modern.hover.
row.ringcolor.interaction.focus.ringobserveduse#e539351px dashed focus ring on controls and rows.
row.bg-selectedcolor.interaction.selection.bgapproveduse#531310Selected row, tab, workspace; D-027 recessed the fill, leaving the 2px indicator and the check to carry selection.
row.text-selectedcolor.interaction.selection.textobserveduse#f4eeeeText on the selection.
row.ring-selectedcolor.interaction.focus.ringobserveduse#e539351px dashed focus ring on controls and rows.

State matrix

Every declared state and combination, statically rendered; the default cell is live. Sizes: compact, comfortable (switch with the density control above).

StateUnifiedSplitWord-levelMerge conflict
default
title.js, old against new, unified
Old lineNew lineChangeText
@@ -3,7 +3,8 @@ export function title(container)
33 const name = container.name ?? "untitled";
4− removed let label = name;
4+ added let label = `${name} (${container.children.length})`;
55 if (label.length > MAX_LENGTH) {
66~ modified label = label.slice(0, MAX_LENGTH) + "…";
77 }
title.js, old on the left, new on the right
Old lineOld changeOld textNew lineNew changeNew text
@@ -3,7 +3,8 @@ export function title(container)
3 const name = container.name ?? "untitled"; 3 const name = container.name ?? "untitled";
4− removed let label = name; 4+ added let label = `${name} (${container.children.length})`;
5 if (label.length > MAX_LENGTH) { 5 if (label.length > MAX_LENGTH) {
6~ modified label = label.slice(0, MAX_LENGTH); 6~ modified label = label.slice(0, MAX_LENGTH) + "…";
7+ added label = label.trimEnd();
title.js, unified with word-level changes
Old lineNew lineChangeText
@@ -4,3 +4,3 @@ export function title(container)
4− removed let label = name;
4+ added let label = `${name} (${count})`;
5− removed if (label.length > 48) {
5+ added if (label.length > MAX_LENGTH) {
title.js, merge conflict between main and feature/gaps
LineChangeText
3 const name = container.name ?? "untitled";
4 <<<<<<< HEAD (main)
5− ours let label = name;
6 =======
7+ theirs let label = `${name} (${count})`;
8 >>>>>>> feature/gaps
9 if (label.length > MAX_LENGTH) {
hover
selected
focus-visible
expanded
collapsed

Purpose

The +, − and ~ markers are content, present in every state and every profile; word-level underlines leave the text contrast untouched.

Anatomy

Root .diff-view with .diff-view-unified, -split, -word-level or -conflict. Rows: .diff-view-row, -row-context, -row-added, -row-removed, -row-modified, -row-hunk, -row-fold, -row-conflict (marker lines -row-conflict-start, -mid, -end). Cells: .diff-view-ln, -marker, -text, -fold-cell, -range (the @@ span). Split adds -side-old and -side-new, and on a -row-pair -cell-removed and -cell-added (an empty -cell-blank faces an added line); conflict adds -row-ours and -row-theirs. Word spans: .diff-view-word-added (2px underline in color.diff.added.emphasis) and .diff-view-word-removed (underline plus line-through in color.diff.removed.emphasis). Fold: .diff-view-fold with SVG chevron .diff-view-chevron and labels -fold-collapsed and -fold-expanded; folded rows are <tbody class="diff-view-folded" hidden>. Line numbers use color.text.muted, not color.code.line-number, which falls below 4.5:1 on the modified fill.

States

StateVisualNon-colour channel
defaultadded color.diff.added.bg, + in color.diff.added.gutter; removed color.diff.removed.bg, − in color.diff.removed.gutter; modified color.diff.modified.bg, ~ in color.diff.modified.gutter; hunks color.diff.header-bg; conflicts in a 3px double color.diff.conflict-borderthe markers; the double border
hovernumber and marker cells → color.interaction.hover.bg; the text cell keeps its fillcursor: default; hover-capable pointers only
selectedwhole row color.interaction.selection.bg with color.interaction.selection.text; the marker staysaria-current (or :target for a permalinked line); the marker glyph
focus-visible1px dashed color.interaction.focus.ring at −2px around the row, including a selected rowthe ring
expandedfolded <tbody> shown; chevron down; “Hide 12 unchanged lines”aria-expanded="true"; the rows appear
collapsedfolded <tbody> hidden; chevron to the inline end; ”… 12 unchanged lines”aria-expanded="false"; the ellipsis

Precedence: selected > hover; focus-visible is always drawn. A selected row loses its diff fill, never its marker.

Keyboard

A plain table has no row focus. The web distribution wraps it in a named, focusable scroll region with the container focus ring, so wide lines can be read without page overflow. The chevron turns within the motion budget.

Accessibility

Each marker sits in its own cell, so a screen reader announces + or − before the line and monochrome print keeps the distinction. Word-level changes use text-decoration, never a fill, so row text keeps 4.5:1 on every diff fill. The − marker is a bold glyph on its own fill and is held to the 3:1 graphics floor. Folded rows are hidden, not visually clipped, so they are not read while collapsed. Conflict markers are the literal <<<<<<<, ======= and >>>>>>> lines.

Portability

Cell backgrounds, borders and text decoration exist in every toolkit. Without double borders, draw two 1px lines and record it; without line-through, keep the underline and the − marker. The three extension hues are confined to the diff group and change with the profile; the markers do not.

Non-examples

Red and green fills without +/−. Word-level changes as a bright fill that pushes text below 4.5:1. A removed line in text.disabled or struck through whole. Collapsed hunks only visually clipped and still read out. Purple conflict blocks. Hover that recolours the code text. Round-cornered hunk cards. A selected row that drops the marker.

Contrast report

29 declared pairs measured on resolved sRGB values with the WCAG 2.x formula; 0 waived (decorative only), 0 failing. A pass is a design target for the named role, not an application-wide conformance claim.

PairStateKindForegroundBackgroundRatioMinResult
diff-view default textdefaulttextcolor.text.default #e99499color.code.bg #0000009.164.5pass
diff-view default borderdefaultuicolor.border.control #c81a1acolor.code.bg #0000003.623pass
diff-view hover texthovertextcolor.text.default #e99499color.interaction.hover.bg #1c10108.104.5pass
diff-view selected textselectedtextcolor.interaction.selection.text #f4eeeecolor.interaction.selection.bg #53131012.474.5pass
diff-view focus-visible textfocus-visibletextcolor.text.default #e99499color.code.bg #0000009.164.5pass
diff-view focus-visible outlinefocus-visibleuicolor.interaction.focus.ring #e53935color.code.bg #0000004.973pass
diff-view expanded textexpandedtextcolor.text.link #f73f35color.diff.header-bg #2410104.944.5pass
diff-view collapsed textcollapsedtextcolor.text.link #f73f35color.diff.header-bg #2410104.944.5pass
text on an added row—textcolor.text.default #e99499color.diff.added.bg #0f1a0e7.804.5pass
text on a removed row—textcolor.text.default #e99499color.diff.removed.bg #2b0e0d7.824.5pass
text on a modified row—textcolor.text.default #e99499color.diff.modified.bg #1f1a0c7.564.5pass
hunk context on the header—textcolor.text.default #e99499color.diff.header-bg #2410107.924.5pass
hunk range on the header—textcolor.text.muted #d4868bcolor.diff.header-bg #2410106.564.5pass
line numbers on a context row—textcolor.text.muted #d4868bcolor.code.bg #0000007.584.5pass
line numbers on an added row—textcolor.text.muted #d4868bcolor.diff.added.bg #0f1a0e6.464.5pass
line numbers on a removed row—textcolor.text.muted #d4868bcolor.diff.removed.bg #2b0e0d6.474.5pass
line numbers on a modified row—textcolor.text.muted #d4868bcolor.diff.modified.bg #1f1a0c6.264.5pass
the + marker—textcolor.diff.added.gutter #82d9a0color.diff.added.bg #0f1a0e10.544.5pass
the − marker (bold, 4.24:1; glyph plus fill)—uicolor.diff.removed.gutter #e53935color.diff.removed.bg #2b0e0d4.243pass
the ~ marker—textcolor.diff.modified.gutter #e5c07bcolor.diff.modified.bg #1f1a0c10.044.5pass
word-level added underline—uicolor.diff.added.emphasis #82d9a0color.diff.added.bg #0f1a0e10.543pass
word-level removed underline and strike—uicolor.diff.removed.emphasis #e53935color.diff.removed.bg #2b0e0d4.243pass
conflict double border—uicolor.diff.conflict-border #f73f35color.code.bg #0000005.723pass
conflict marker lines—textcolor.text.bright #ffa2a7color.diff.header-bg #2410109.494.5pass
ring on an added rowfocus-visibleuicolor.interaction.focus.ring #e53935color.diff.added.bg #0f1a0e4.233pass
ring on a removed rowfocus-visibleuicolor.interaction.focus.ring #e53935color.diff.removed.bg #2b0e0d4.243pass
ring on a modified rowfocus-visibleuicolor.interaction.focus.ring #e53935color.diff.modified.bg #1f1a0c4.103pass
ring on the selected rowfocus-visibleuicolor.interaction.focus.ring #e53935color.interaction.selection.bg #5313103.383pass
fold button text—textcolor.text.link #f73f35color.diff.header-bg #2410104.944.5pass

Keys

KeyAction
Tab / Shift+TabMoves between the fold buttons and any row the host makes focusable; rows are not stops in a plain table.
Enter / SpaceOn a fold button toggles the hidden hunk; focus stays on the button.
Down / UpIn grid hosts, moves the selected row; the theme draws the ring on whichever row the host focuses.
(host)Next-change and previous-change navigation, staging and comment affordances are host bindings.

For agents

Component JSON ·Implementation brief ·Build a task kit ·

Related: code-editor diagnostics terminal

Stress fixtures: FX-STATE-MATRIX FX-320 FX-ZOOM-200 FX-RM FX-HC FX-LONG FX-OVERFLOW