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
| Old line | New line | Change | Text |
|---|---|---|---|
| @@ -3,7 +3,8 @@ export function title(container) | |||
| 3 | 3 | const name = container.name ?? "untitled"; | |
| 4 | − removed | let label = name; | |
| 4 | + added | let label = `${name} (${container.children.length})`; | |
| 5 | 5 | if (label.length > MAX_LENGTH) { | |
| 6 | 6 | ~ modified | label = label.slice(0, MAX_LENGTH) + "…"; |
| 7 | 7 | } | |
| 8 | 8 | return escape(label); | |
| 9 | 9 | } | |
| 10 | 10 | … ten more context lines … | |
<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 > 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
| Old line | Old change | Old text | New line | New change | New 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(); | |||
| 7 | } | 8 | } | ||
| 8 | return escape(label); | 9 | return escape(label); | ||
<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 > 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 > 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
| Old line | New line | Change | Text |
|---|---|---|---|
| @@ -4,3 +4,3 @@ export function title(container) | |||
| 4 | − removed | let label = |
|
| 4 | + added | let label = `${name} (${count})`; | |
| 5 | − removed | if (label.length > |
|
| 5 | + added | if (label.length > MAX_LENGTH) { | |
| 6 | 6 | label = label.slice(0, MAX_LENGTH) + "…"; | |
| 7 | 7 | } | |
<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 > <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 > <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
| Line | Change | Text |
|---|---|---|
| 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) { | |
| 10 | label = label.slice(0, MAX_LENGTH) + "…"; | |
| 11 | } | |
<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"><<<<<<< 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">>>>>>>> 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 > 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.0import '@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.
Copy the complete implementation
npx --package=@j3w1/ui@4.0.0 j3w1-ui copy diff-view --out ./vendor/j3w1/diff-viewThe 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
| Name | Type / signature | Meaning |
|---|---|---|
disabled | CustomEvent | Disable the native controls owned by this instance. |
loading | CustomEvent | Mark the primary control busy and prevent duplicate activation. |
properties
| Name | Type / signature | Meaning |
|---|---|---|
disabled | boolean | Delegates disabled state to native children. |
name | string | Native control name; application form keys remain explicit. |
methods
| Name | Type / signature | Meaning |
|---|---|---|
refresh | (): void | Reconnect behavior after replacing native child structure. |
focus | (options?: FocusOptions): void | Focus the first usable native control. |
setText | (text: string): void | Replace the bounded read-only text view; no code or shell execution occurs. |
events
| Name | Type / signature | Meaning |
|---|---|---|
j3w1-action | CustomEvent | Request 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
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.
| Part | Role | Value | Use |
|---|---|---|---|
root.bg | color.code.bgapproveduse | #000000 | D-041 code.bg: color.primitive.ink.0. |
root.border | color.border.controlapproveduse | #c81a1a | D-041 owner correction: red control and floating-frame borders, distinct from rose text. |
root.text | color.text.defaultapproveduse | #e99499 | D-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.text | color.text.mutedapproveduse | #d4868b | D-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.rule | color.code.gutter-ruleapproveduse | #482b2b | D-041 code.gutter-rule: color.primitive.modern.divider. |
row.divider | color.border.dividerapproveduse | #482b2b | D-041 border.divider: color.primitive.modern.divider. |
added.bg | color.diff.added.bgapproveduse | #0f1a0e | Added line background. |
added.gutter | color.diff.added.gutterapproveduse | #82d9a0 | D-041 diff.added.gutter: color.primitive.modern.green. |
added.emphasis | color.diff.added.emphasisapproveduse | #82d9a0 | D-041 diff.added.emphasis: color.primitive.modern.green. |
removed.bg | color.diff.removed.bgobserveduse | #2b0e0d | Removed line background. |
removed.gutter | color.diff.removed.gutterobserveduse | #e53935 | Removed gutter mark and −. |
removed.emphasis | color.diff.removed.emphasisobserveduse | #e53935 | Word-level removal: underline and line-through. |
modified.bg | color.diff.modified.bgapproveduse | #1f1a0c | Modified line background. |
modified.gutter | color.diff.modified.gutterapproveduse | #e5c07b | D-041 diff.modified.gutter: color.primitive.modern.amber. |
hunk.bg | color.diff.header-bgobserveduse | #241010 | Hunk header background. |
hunk.text | color.text.mutedapproveduse | #d4868b | D-041 owner correction: rose is the default UI and reading foreground; near-white is reserved for document headings, highlights and contrast-critical fills. |
hunk.context | color.text.defaultapproveduse | #e99499 | D-041 owner correction: rose is the default UI and reading foreground; near-white is reserved for document headings, highlights and contrast-critical fills. |
conflict.border | color.diff.conflict-borderobserveduse | #f73f35 | Conflict block: double 1px border. |
conflict.marker | color.text.brightapproveduse | #ffa2a7 | D-041 owner correction: rose is the default UI and reading foreground; near-white is reserved for document headings, highlights and contrast-critical fills. |
fold.text | color.text.linkapproveduse | #f73f35 | Strong red link text, distinct from rose body text and headings; retain the underline. |
fold.bg-hover | color.interaction.hover.bg-strongapproveduse | #1c1010 | D-041 interaction.hover.bg-strong: color.primitive.modern.hover. |
row.bg-hover | color.interaction.hover.bgapproveduse | #1c1010 | D-041 interaction.hover.bg: color.primitive.modern.hover. |
row.ring | color.interaction.focus.ringobserveduse | #e53935 | 1px dashed focus ring on controls and rows. |
row.bg-selected | color.interaction.selection.bgapproveduse | #531310 | Selected row, tab, workspace; D-027 recessed the fill, leaving the 2px indicator and the check to carry selection. |
row.text-selected | color.interaction.selection.textobserveduse | #f4eeee | Text on the selection. |
row.ring-selected | color.interaction.focus.ringobserveduse | #e53935 | 1px 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).
| State | Unified | Split | Word-level | Merge conflict | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
default |
|
|
|
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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
| State | Visual | Non-colour channel |
|---|---|---|
| default | added 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-border | the markers; the double border |
| hover | number and marker cells → color.interaction.hover.bg; the text cell keeps its fill | cursor: default; hover-capable pointers only |
| selected | whole row color.interaction.selection.bg with color.interaction.selection.text; the marker stays | aria-current (or :target for a permalinked line); the marker glyph |
| focus-visible | 1px dashed color.interaction.focus.ring at −2px around the row, including a selected row | the ring |
| expanded | folded <tbody> shown; chevron down; “Hide 12 unchanged lines” | aria-expanded="true"; the rows appear |
| collapsed | folded <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.
| Pair | State | Kind | Foreground | Background | Ratio | Min | Result |
|---|---|---|---|---|---|---|---|
| diff-view default text | default | text | color.text.default #e99499 | color.code.bg #000000 | 9.16 | 4.5 | pass |
| diff-view default border | default | ui | color.border.control #c81a1a | color.code.bg #000000 | 3.62 | 3 | pass |
| diff-view hover text | hover | text | color.text.default #e99499 | color.interaction.hover.bg #1c1010 | 8.10 | 4.5 | pass |
| diff-view selected text | selected | text | color.interaction.selection.text #f4eeee | color.interaction.selection.bg #531310 | 12.47 | 4.5 | pass |
| diff-view focus-visible text | focus-visible | text | color.text.default #e99499 | color.code.bg #000000 | 9.16 | 4.5 | pass |
| diff-view focus-visible outline | focus-visible | ui | color.interaction.focus.ring #e53935 | color.code.bg #000000 | 4.97 | 3 | pass |
| diff-view expanded text | expanded | text | color.text.link #f73f35 | color.diff.header-bg #241010 | 4.94 | 4.5 | pass |
| diff-view collapsed text | collapsed | text | color.text.link #f73f35 | color.diff.header-bg #241010 | 4.94 | 4.5 | pass |
| text on an added row | — | text | color.text.default #e99499 | color.diff.added.bg #0f1a0e | 7.80 | 4.5 | pass |
| text on a removed row | — | text | color.text.default #e99499 | color.diff.removed.bg #2b0e0d | 7.82 | 4.5 | pass |
| text on a modified row | — | text | color.text.default #e99499 | color.diff.modified.bg #1f1a0c | 7.56 | 4.5 | pass |
| hunk context on the header | — | text | color.text.default #e99499 | color.diff.header-bg #241010 | 7.92 | 4.5 | pass |
| hunk range on the header | — | text | color.text.muted #d4868b | color.diff.header-bg #241010 | 6.56 | 4.5 | pass |
| line numbers on a context row | — | text | color.text.muted #d4868b | color.code.bg #000000 | 7.58 | 4.5 | pass |
| line numbers on an added row | — | text | color.text.muted #d4868b | color.diff.added.bg #0f1a0e | 6.46 | 4.5 | pass |
| line numbers on a removed row | — | text | color.text.muted #d4868b | color.diff.removed.bg #2b0e0d | 6.47 | 4.5 | pass |
| line numbers on a modified row | — | text | color.text.muted #d4868b | color.diff.modified.bg #1f1a0c | 6.26 | 4.5 | pass |
| the + marker | — | text | color.diff.added.gutter #82d9a0 | color.diff.added.bg #0f1a0e | 10.54 | 4.5 | pass |
| the − marker (bold, 4.24:1; glyph plus fill) | — | ui | color.diff.removed.gutter #e53935 | color.diff.removed.bg #2b0e0d | 4.24 | 3 | pass |
| the ~ marker | — | text | color.diff.modified.gutter #e5c07b | color.diff.modified.bg #1f1a0c | 10.04 | 4.5 | pass |
| word-level added underline | — | ui | color.diff.added.emphasis #82d9a0 | color.diff.added.bg #0f1a0e | 10.54 | 3 | pass |
| word-level removed underline and strike | — | ui | color.diff.removed.emphasis #e53935 | color.diff.removed.bg #2b0e0d | 4.24 | 3 | pass |
| conflict double border | — | ui | color.diff.conflict-border #f73f35 | color.code.bg #000000 | 5.72 | 3 | pass |
| conflict marker lines | — | text | color.text.bright #ffa2a7 | color.diff.header-bg #241010 | 9.49 | 4.5 | pass |
| ring on an added row | focus-visible | ui | color.interaction.focus.ring #e53935 | color.diff.added.bg #0f1a0e | 4.23 | 3 | pass |
| ring on a removed row | focus-visible | ui | color.interaction.focus.ring #e53935 | color.diff.removed.bg #2b0e0d | 4.24 | 3 | pass |
| ring on a modified row | focus-visible | ui | color.interaction.focus.ring #e53935 | color.diff.modified.bg #1f1a0c | 4.10 | 3 | pass |
| ring on the selected row | focus-visible | ui | color.interaction.focus.ring #e53935 | color.interaction.selection.bg #531310 | 3.38 | 3 | pass |
| fold button text | — | text | color.text.link #f73f35 | color.diff.header-bg #241010 | 4.94 | 4.5 | pass |
Keys
| Key | Action |
|---|---|
| Tab / Shift+Tab | Moves between the fold buttons and any row the host makes focusable; rows are not stops in a plain table. |
| Enter / Space | On a fold button toggles the hidden hunk; focus stays on the button. |
| Down / Up | In 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