Styling Diff and Comparison Views with Tailwind CSS
AI generated
tw
Tailwind CSS · UI Pattern · Diff View
Diff and Comparison Views
Side-by-side and inline, consistent color coding and line numbers with Tailwind CSS

A diff view has to show at a glance what was added, removed, or changed, whether as a side-by-side comparison of two columns or a more compact inline representation. This pattern covers coloring both variants consistently with Tailwind CSS, adding line numbers, and walking through a practical text comparison between two versions.

14 min read Side-by-side vs. inline Color coding & line numbers

1. Why a diff view needs more than three colors

A diff view compares two versions of content, usually text or code, and has to make three fundamentally different kinds of change distinguishable: added lines, removed lines, and changed lines, where part of the content stays and another part gets replaced. If all three states are only distinguished by a single accent color, say all three blue at different opacity levels, the user has to read the line content itself to figure out the type of change, which defeats the actual purpose of a diff view.

The established convention from version control systems like Git is green for additions, red for removals, and a third color, usually yellow or orange, for changes or for highlighted word ranges within an otherwise unchanged line. This convention shouldn't be broken without good reason, since developers and technically savvy users have already internalized it from terminals, IDEs, and pull request views, and a diverging color choice creates unnecessary cognitive friction.

2. Side-by-side and inline layout with Tailwind Grid

The side-by-side view places the old and new versions next to each other in two columns, with removed lines appearing only on the left and added lines only on the right, while unchanged lines sit at the same height on both sides. CSS Grid with two equally wide columns via grid-cols-2 handles this cleanly; what matters is inserting empty placeholder lines on the other side, so lines stay synchronized at the same height despite the two versions having different lengths.

The inline view, by contrast, shows all lines stacked in a single column, in the order they appear in the merged diff computation. It needs noticeably less horizontal space and therefore suits narrow screens or embedded previews better, but loses the direct visual same-height comparison that the side-by-side view offers. In practice, a toggle between both modes, often driven by an Alpine boolean, works well so users can choose based on screen width and preference.


<div x-data="{ mode: 'side-by-side' }">
  <div class="mb-3 flex gap-2 text-sm">
    <button @click="mode = 'side-by-side'" :class="mode === 'side-by-side' && 'font-semibold text-blue-700'">
      Side by side
    </button>
    <button @click="mode = 'inline'" :class="mode === 'inline' && 'font-semibold text-blue-700'">
      Inline
    </button>
  </div>

  <div x-show="mode === 'side-by-side'" class="grid grid-cols-2 divide-x divide-slate-200 font-mono text-sm">
    <div class="bg-red-50">
      <div class="flex px-2 py-0.5">
        <span class="w-8 shrink-0 select-none text-right text-slate-400">12</span>
        <span class="pl-3 text-red-800">- return $legacyPrice;</span>
      </div>
    </div>
    <div class="bg-green-50">
      <div class="flex px-2 py-0.5">
        <span class="w-8 shrink-0 select-none text-right text-slate-400">12</span>
        <span class="pl-3 text-green-800">+ return $priceFormatter->format($price);</span>
      </div>
    </div>
  </div>

  <div x-show="mode === 'inline'" class="divide-y divide-slate-100 font-mono text-sm">
    <div class="flex bg-red-50 px-2 py-0.5">
      <span class="w-8 shrink-0 select-none text-right text-slate-400">12</span>
      <span class="pl-3 text-red-800">- return $legacyPrice;</span>
    </div>
    <div class="flex bg-green-50 px-2 py-0.5">
      <span class="w-8 shrink-0 select-none text-right text-slate-400">12</span>
      <span class="pl-3 text-green-800">+ return $priceFormatter->format($price);</span>
    </div>
  </div>
</div>

3. Consistent color coding across the whole view

For color coding to actually function as a reliable signal, it must be used identically everywhere in the diff view: the same shade of green for the line background, the line number, a possible plus symbol at the start of the line, and any counter in a summary above the diff view. A break in that consistency, say a stronger green in the background than in the counter, makes users subconsciously doubt whether it's really the same category.

In practice, a fixed mapping of Tailwind color steps per state works well, for example green-50 for the line background and green-800 for the text on additions, mirrored by red-50 and red-800 for removals. This mapping should be defined centrally as a constant or Tailwind component rather than reassembling the matching classes by hand in every single diff line, since a later adjustment to the color palette would otherwise potentially touch dozens of places in the code.

4. Aligning the line number column correctly

Line numbers need right alignment so single- and multi-digit numbers end at the same horizontal position despite differing character counts, and the diff content itself starts along a straight line. A fixed column width via w-8 or w-10, depending on how many lines the longest expected file has, also prevents the column from suddenly growing wider than the rest of the view for particularly long files with four-digit line numbers.

In the side-by-side view, each of the two columns needs its own independent line numbering, since the old and new versions can have different line counts and an added line in the new version simply has no corresponding number in the old one. For lines with no counterpart on the other side, the line number stays blank rather than showing a zero or a placeholder like a dash, which could easily be mistaken for an actual line number in diff tools like Git.

5. Practical example: comparing two text versions

A simple but instructive use case is comparing two short text paragraphs, for example two versions of a product description before and after an editorial revision. The diff algorithm itself, usually a line- or word-based longest-common-subsequence computation, returns a list of operations: unchanged, added, removed. That list then simply renders line by line with the matching Tailwind classes from the color coding, without the presentation layer needing to contain any diff logic of its own.

For word-level rather than pure line-level comparisons, an extra highlight on individual changed words within an otherwise identical line is worth adding, for example via a mark element with a yellow background. That makes small changes like a corrected number or a swapped adjective far more visible than marking the entire line as changed across the board, which lets the actual change get lost easily in mostly identical long lines.

6. Adapting diff colors for dark mode

The light background colors like green-50 and red-50, which work well in light mode, look like a jarring foreign object on a dark background in dark mode if carried over unchanged. Tailwind's dark: prefix lets you define darker, more saturated background tones for each state, for example dark:bg-green-950 and dark:bg-red-950, combined with correspondingly lighter text colors like dark:text-green-300, so contrast stays comparable across both modes.

It matters to keep the color mapping consistent between light and dark mode, so green always stays green rather than shifting to a different hue in dark mode just because a particular green tone was hard to read in the dark. A quick test pass with realistic diff content in both modes also reveals more reliably whether contrast is actually sufficient than a purely computed contrast check of individual color pairs.

7. Accessibility: don't rely on color alone

Anyone unable to distinguish colors, for example due to red-green color blindness, which affects a meaningful share of the male population, shouldn't have to rely exclusively on background color in a diff view to tell additions from removals. A plus and a minus symbol at the start of the line, as most terminal diffs already use anyway, provides a second, color-independent signal and should be present by default in every diff view, not just as an optional extra.

For screen reader users, the visual marking alone isn't enough regardless. Every diff line should communicate through an aria-label or hidden text whether it's an added, removed, or unchanged line, for example aria-label="Added line" on the wrapping element. Without this textual addition, the entire point of the diff view stays hidden from screen reader users, even when color and symbol are perfectly clear for sighted users.

8. Long lines: wrapping instead of horizontal scrolling

Code lines or long sentences quickly exceed the available width of the diff column, especially in the side-by-side view, where each column only has half the total width available. Horizontal scrolling per line is technically possible but forces users to scroll again for every long line to see the full content, which quickly becomes tedious in a diff with many long lines.

Automatic wrapping via whitespace-pre-wrap instead of whitespace-pre solves this problem but produces lines of varying visual height, which complicates exact line-to-line alignment between the left and right columns in the side-by-side view. In practice a compromise is common: automatic wrapping in the inline view, where exact line height doesn't matter, and horizontal scrolling with a visible scrollbar in the side-by-side view, to preserve alignment.

9. Limits of the pattern and common mistakes

A common mistake is running the diff computation itself client-side for very large files, for example an entire generated file with tens of thousands of lines. A naive diff algorithm has a runtime in such cases that can block the browser tab for several seconds, which is why server-side computation or at least a web worker running the computation off the main thread, keeping the UI responsive, is worth it for large comparisons.

A second limit concerns pure whitespace changes, for example a changed indentation with no content change, which a naive line-based diff computation shows as a fully changed line even though nothing meaningful happened content-wise. A toggle that optionally ignores whitespace changes, similar to the option in Git diffs, significantly reduces visual noise in such cases and makes the genuinely relevant changes stand out more clearly.

State Background (light) Background (dark) Additional signal
Added bg-green-50, text-green-800 bg-green-950, text-green-300 Plus symbol at line start
Removed bg-red-50, text-red-800 bg-red-950, text-red-300 Minus symbol at line start
Changed (word level) bg-yellow-100 on mark bg-yellow-900/40 on mark Highlight only the changed word range
Unchanged bg-transparent bg-transparent No additional symbol needed

Mironsoft

Tailwind CSS architecture, design systems, and performance

Tailwind frontends that stay maintainable despite thousands of utility classes?

We review existing Tailwind projects for bloated class lists, inconsistent design tokens, and unused CSS remnants, then build a design system that scales cleanly instead of getting messier with every component.

Design System Review

Checking tokens, spacing scale, and component consistency for maintainability.

Performance Optimization

Systematically reducing CSS bundle size, purge configuration, and load times.

Component Architecture

Building reusable, well-structured components instead of sprawling class lists.

10. Summary

Diff Comparison View with Tailwind: The Essentials at a Glance

Color convention

Green for added, red for removed, yellow for changed, consistent across background, text, and summary.

Side-by-side vs. inline

Two grid columns with placeholder lines versus a single column in diff order, often selectable via a toggle.

Accessibility

Plus/minus symbols and aria-label supplement the color coding so colorblind and screen reader users aren't excluded.

Long lines

whitespace-pre-wrap in the inline view, horizontal scrolling with preserved line alignment in the side-by-side view.

11. FAQ: Diff Comparison View with Tailwind: The Essentials at a Glance

1Which colors should I use for a diff view?
Green for added, red for removed, and yellow or orange for changed content, following the established convention from Git and most IDEs.
2What's the difference between side-by-side and inline diff?
Side-by-side shows the old and new version in two columns next to each other, inline shows all lines in a single column in the order of the diff computation.
3How do I align line numbers in a diff view?
Right-aligned within a fixed column width, so single- and multi-digit numbers end at the same position and the diff content starts along a straight line.
4Why isn't color alone enough for diff marking?
Because users with red-green color blindness can't otherwise distinguish the states. Plus and minus symbols provide a second, color-independent signal.
5How do I mark diff lines for screen readers?
Via an aria-label or hidden text on every line that explicitly names the state, for example Added line or Removed line.
6How do I handle very long code lines in a diff view?
In the inline view with automatic wrapping via whitespace-pre-wrap, in the side-by-side view usually with horizontal scrolling to preserve line alignment between columns.
7How do I adapt diff colors for dark mode?
With darker, more saturated background tones via the dark: prefix, for example dark:bg-green-950 instead of bg-green-50, combined with lighter text colors for sufficient contrast.
8How do I highlight individual changed words instead of whole lines?
With a mark element with a yellow background around the changed word range, while the rest of the line stays rendered unchanged.
9Why does diff computation get slow on large files?
Because a naive line-based diff algorithm's runtime grows disproportionately with file size. For large files, server-side computation or a web worker pays off.
10How do I filter out pure whitespace changes from a diff view?
With a toggle that ignores whitespace differences during comparison, similar to the corresponding option in Git, to reduce visual noise.