Skip to content
Template Empire
Browse components

Text Replace Diff

Mark the changed part of live inline copy while keeping native text.

Edition workspace

Publish one collection in print

Keep every edition together in one workspace.

Your publishing plan

Customise

Code

// Pass your current controlled string; initial text is unmarked.
const currentCopy = "three collections in print";

<TextReplaceDiff>
  {currentCopy}
</TextReplaceDiff>

Text Replace Diff is a free React text effect for controlled copy changes. It displays the current string immediately and briefly fades a wash behind its replacement window. Use it for a configurator value or editorial date that changes after the reader makes a choice. It uses CSS opacity with native grapheme segmentation and needs no animation engine.

The current string stays one selectable text node. Common grapheme prefixes and suffixes stay unmarked; multiple edits share one middle window rather than a minimal edit script. The baseline-aligned inline-block is atomic in its parent line and can wrap internally. It is a value box, with the same line and bidi boundary as a plain inline-block, rather than arbitrary paragraph integration.

Install

Add the @te registry to your components.json once:

{
  "registries": {
    "@te": "https://templateempire.io/r/{name}.json"
  }
}

Then install the component:

npx shadcn@latest add @te/text-replace-diff

Usage

"use client";

import { useState } from "react";
import { TextReplaceDiff } from "@/components/text-replace-diff";

export function Example() {
  const [multiple, setMultiple] = useState(false);
  return (
    <div>
      <p>Publish <TextReplaceDiff>
        {multiple ? "three collections in print" : "one collection in print"}
      </TextReplaceDiff></p>
      <button onClick={() => setMultiple(!multiple)}>Change collection plan</button>
    </div>
  );
}

Props

PropTypeDefaultDescription
childrenstringrequiredComplete current plain text. It updates immediately; empty strings are accepted.
animateChangesbooleantrueEnable future change marks. Turning off cancels; turning on establishes the current baseline without replay.
classNamestringundefinedMerged onto the value box. Adjust typography, width, wrapping or block display. Fragmented inline/contents or a removed positioned paint stack uses plain text.

Customisation

Scope --text-diff-mark, --text-diff-duration and --text-diff-ease on an ancestor. Defaults are color-mix(in oklab, var(--primary) 18%, var(--background)), var(--dur-3, 600ms) and var(--ease-out-quart, cubic-bezier(0.25, 1, 0.5, 1)). The host supplies --primary, --background and a readable inherited foreground; check contrast against your peak wash. Positive CSS time and easing values are supported. The wash holds for the first 20% of its duration, then fades. Current text never fades.

<p className="[--text-diff-duration:900ms] [--text-diff-mark:var(--accent)]">
  Meet the studio on <TextReplaceDiff className="max-w-60">
    {selectedDate}
  </TextReplaceDiff>
</p>

Frequently asked questions

Which part of the new text is marked?
The single current window between the longest common grapheme prefix and non-overlapping suffix. Tuesday → Thursday marks hur. Two separated edits can include unchanged interior text in one window. It does not render deletions or outgoing history. Identical, deletion-only, empty and whitespace-only windows have no mark.
Does the text keep native selection, shaping and wrapping?
Yes, within its inline-block value box: current text is native, with no letter wrappers or duplicate string. The box can move as a whole to the next parent line and wraps internally with pre-wrap and overflow-wrap:anywhere; a wrapped box uses its native last-line baseline. Joining across the box boundary and preservation of old selection offsets when the string changes are not promised. An intersecting selection suppresses or cancels decoration. A surrounding native link retains its name and interaction.
What happens with reduced motion, missing APIs or unsupported layout?
Initial/SSR/no-JS content is complete and unmarked. Reduced motion, disabled enhancement, hidden/offscreen updates and resume never queue a replay. Missing required observer APIs or Segmenter, editable/vertical/transformed/zoomed ancestry, fragmented inline/contents layout, unusable geometry or more than 12 range fragments use plain text. Either compared string above 2,048 UTF-16 code units or 512 graphemes also uses plain text without truncation.
Can the marker track a moving or resizing layout?
This version measures once per eligible committed change and cancels on resize, captured scroll, font events, root/ancestor class/style/dir changes or ancestor animation/transition start. It supports ordinary untransformed horizontal inline-block/block geometry. Place it in stable layout; it does not chase third-party movement or animate text layout.
Does it announce changes to assistive technology?
It adds no live region, focus target or accessibility duplicate. The current native text provides the content and the wash is decorative. Consumers needing a specific announcement own a separate status region. The repository uses MIT with Commons Clause; read its licence before redistribution.

Related components

Build the rest of your publishing siteExplore complete Next.js templates from Template Empire.Explore templates