export function sign(id) { const t = make(id); log(t); return t;}export function sign(id) { const t = make(id); log(t); return t;}import { useState } from 'react';
type Line = { n: string; text: string; kind?: 'del' | 'add' };
const LINES: Line[] = [
{ n: '1', text: 'export function sign(id) {' },
{ n: '2', text: ' const t = make(id);', kind: 'del' },
{ n: '2', text: ' const t = make(id, 900);', kind: 'add' },
{ n: '3', text: ' log(t);', kind: 'del' },
{ n: '3', text: ' return t;' },
{ n: '4', text: '}' },
];
const css = `
.card-diff { box-sizing: border-box; width: 240px; padding: 12px 0 10px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-diff__head { display: flex; align-items: center; gap: 8px; padding: 0 12px 10px; }
.card-diff__file { font: 600 12px ui-monospace, SFMono-Regular, Menlo, monospace; color: light-dark(#373a40, #f1f2f3); }
.card-diff__stat { font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; color: light-dark(#5f626a, #a7abb2); opacity: 0; transition: opacity 0.3s; }
.card-diff--after .card-diff__stat { opacity: 1; }
.card-diff__tabs { display: flex; margin-left: auto; padding: 2px; border-radius: 7px; background: light-dark(#ebedf0, #232529); }
.card-diff__tab { height: 22px; padding: 0 8px; border: 0; border-radius: 5px; background: none; color: light-dark(#5f626a, #a7abb2); font: inherit; font-size: 10.5px; font-weight: 600; cursor: pointer; }
.card-diff__tab[aria-pressed='true'] { background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); }
.card-diff__tab:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 1px; }
.card-diff__code { margin: 0; font: 10.5px/1.75 ui-monospace, SFMono-Regular, Menlo, monospace; color: light-dark(#42454c, #e5e6e9); }
/* A row folds open or shut by animating its grid track between 0fr and 1fr. */
.card-diff__row { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
.card-diff__row > div { display: flex; min-height: 0; overflow: hidden; }
.card-diff__n { flex: none; width: 30px; padding-right: 8px; text-align: right; color: light-dark(#5f626a, #a7abb2); }
.card-diff__text { position: relative; white-space: pre; }
.card-diff__row--del > div { background: light-dark(#ebedf0, #232529); }
.card-diff__row--add > div { background: light-dark(#ebedf0, #232529); box-shadow: inset 2px 0 light-dark(#373a40, #f1f2f3); }
.card-diff__text::after { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: light-dark(#50535b, #c6c8cd); transform: scaleX(0); transform-origin: left; }
.card-diff--after .card-diff__row--del { grid-template-rows: 0fr; transition-delay: 0.3s; }
.card-diff--after .card-diff__row--del .card-diff__text::after { transform: scaleX(1); transition: transform 0.3s ease-out; }
.card-diff__row--add { grid-template-rows: 0fr; }
.card-diff--after .card-diff__row--add { grid-template-rows: 1fr; transition-delay: 0.45s; }
@media (prefers-reduced-motion: reduce) {
.card-diff__row, .card-diff--after .card-diff__row--del, .card-diff--after .card-diff__row--add { transition: none; }
.card-diff--after .card-diff__row--del .card-diff__text::after { transition: none; }
}
`;
export function DiffCard() {
const [after, setAfter] = useState(false);
return (
<>
<style>
{css}
</style>
<article className={`card-diff${after ? ' card-diff--after' : ''}`}>
<div className="card-diff__head">
<span className="card-diff__file">sign.ts</span>
<span className="card-diff__stat" aria-hidden={!after}>
+1 −2
</span>
<div className="card-diff__tabs" role="group" aria-label="Version">
{[false, true].map((a) => (
<button key={String(a)} type="button" className="card-diff__tab" aria-pressed={after === a} onClick={() => setAfter(a)}>
{a ? 'After' : 'Before'}
</button>
))}
</div>
</div>
<div className="card-diff__code">
{LINES.map((l, i) => {
const hidden = (l.kind === 'del' && after) || (l.kind === 'add' && !after);
return (
<div key={i} className={`card-diff__row${l.kind ? ` card-diff__row--${l.kind}` : ''}`} aria-hidden={hidden}>
<div>
<span className="card-diff__n">{after && l.kind ? (l.kind === 'add' ? '+' : '−') : l.n}</span>
<code className="card-diff__text">{l.text}</code>
</div>
</div>
);
})}
</div>
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardDiff(el) on the mounted element instead.
<article class="card-diff">
<div class="card-diff__head">
<span class="card-diff__file">sign.ts</span>
<span class="card-diff__stat" aria-hidden="true">+1 −2</span>
<div class="card-diff__tabs" role="group" aria-label="Version">
<button type="button" class="card-diff__tab" aria-pressed="true">Before</button>
<button type="button" class="card-diff__tab" aria-pressed="false">After</button>
</div>
</div>
<div class="card-diff__code">
<div class="card-diff__row" aria-hidden="false">
<div>
<span class="card-diff__n">1</span>
<code class="card-diff__text">export function sign(id) {</code>
</div>
</div>
<div class="card-diff__row card-diff__row--del" aria-hidden="false">
<div>
<span class="card-diff__n">2</span>
<code class="card-diff__text"> const t = make(id);</code>
</div>
</div>
<div class="card-diff__row card-diff__row--add" aria-hidden="true">
<div>
<span class="card-diff__n">2</span>
<code class="card-diff__text"> const t = make(id, 900);</code>
</div>
</div>
<div class="card-diff__row card-diff__row--del" aria-hidden="false">
<div>
<span class="card-diff__n">3</span>
<code class="card-diff__text"> log(t);</code>
</div>
</div>
<div class="card-diff__row" aria-hidden="false">
<div>
<span class="card-diff__n">3</span>
<code class="card-diff__text"> return t;</code>
</div>
</div>
<div class="card-diff__row" aria-hidden="false">
<div>
<span class="card-diff__n">4</span>
<code class="card-diff__text">}</code>
</div>
</div>
</div>
</article>.card-diff { box-sizing: border-box; width: 240px; padding: 12px 0 10px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-diff__head { display: flex; align-items: center; gap: 8px; padding: 0 12px 10px; }
.card-diff__file { font: 600 12px ui-monospace, SFMono-Regular, Menlo, monospace; color: light-dark(#373a40, #f1f2f3); }
.card-diff__stat { font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; color: light-dark(#5f626a, #a7abb2); opacity: 0; transition: opacity 0.3s; }
.card-diff--after .card-diff__stat { opacity: 1; }
.card-diff__tabs { display: flex; margin-left: auto; padding: 2px; border-radius: 7px; background: light-dark(#ebedf0, #232529); }
.card-diff__tab { height: 22px; padding: 0 8px; border: 0; border-radius: 5px; background: none; color: light-dark(#5f626a, #a7abb2); font: inherit; font-size: 10.5px; font-weight: 600; cursor: pointer; }
.card-diff__tab[aria-pressed='true'] { background: light-dark(#373a40, #f1f2f3); color: light-dark(#fbfcfd, #121417); }
.card-diff__tab:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 1px; }
.card-diff__code { margin: 0; font: 10.5px/1.75 ui-monospace, SFMono-Regular, Menlo, monospace; color: light-dark(#42454c, #e5e6e9); }
/* A row folds open or shut by animating its grid track between 0fr and 1fr. */
.card-diff__row { display: grid; grid-template-rows: 1fr; transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
.card-diff__row > div { display: flex; min-height: 0; overflow: hidden; }
.card-diff__n { flex: none; width: 30px; padding-right: 8px; text-align: right; color: light-dark(#5f626a, #a7abb2); }
.card-diff__text { position: relative; white-space: pre; }
.card-diff__row--del > div { background: light-dark(#ebedf0, #232529); }
.card-diff__row--add > div { background: light-dark(#ebedf0, #232529); box-shadow: inset 2px 0 light-dark(#373a40, #f1f2f3); }
.card-diff__text::after { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: light-dark(#50535b, #c6c8cd); transform: scaleX(0); transform-origin: left; }
.card-diff--after .card-diff__row--del { grid-template-rows: 0fr; transition-delay: 0.3s; }
.card-diff--after .card-diff__row--del .card-diff__text::after { transform: scaleX(1); transition: transform 0.3s ease-out; }
.card-diff__row--add { grid-template-rows: 0fr; }
.card-diff--after .card-diff__row--add { grid-template-rows: 1fr; transition-delay: 0.45s; }
@media (prefers-reduced-motion: reduce) {
.card-diff__row, .card-diff--after .card-diff__row--del, .card-diff--after .card-diff__row--add { transition: none; }
.card-diff--after .card-diff__row--del .card-diff__text::after { transition: none; }
}function initCardDiff(root) {
var tabs = root.querySelectorAll('.card-diff__tab');
var stat = root.querySelector('.card-diff__stat');
var rows = root.querySelectorAll('.card-diff__row');
var nums = [];
rows.forEach(function (row) {
nums.push(row.querySelector('.card-diff__n').textContent);
});
// One class swaps the versions: the CSS strikes and folds the removed rows, then opens the added ones.
function set(after) {
root.classList.toggle('card-diff--after', after);
tabs[0].setAttribute('aria-pressed', String(!after));
tabs[1].setAttribute('aria-pressed', String(after));
stat.setAttribute('aria-hidden', String(!after));
rows.forEach(function (row, i) {
var del = row.classList.contains('card-diff__row--del');
var add = row.classList.contains('card-diff__row--add');
row.setAttribute('aria-hidden', String((del && after) || (add && !after)));
// After the change the gutter marks each changed line + or −.
row.querySelector('.card-diff__n').textContent = after && (del || add) ? (add ? '+' : '−') : nums[i];
});
}
function onBefore() {
set(false);
}
function onAfter() {
set(true);
}
tabs[0].addEventListener('click', onBefore);
tabs[1].addEventListener('click', onAfter);
return function () {
tabs[0].removeEventListener('click', onBefore);
tabs[1].removeEventListener('click', onAfter);
};
}
document.querySelectorAll('.card-diff').forEach(initCardDiff);Goal: Create a "Diff" card.
Visual behavior: A code card with Before and After: removed lines get struck through and fold away, added lines open up in their place, and the gutter marks each change with + or −.
Technique:
- Each line is a grid row whose single track animates between 1fr and 0fr, so it folds to nothing or opens to its natural height without measuring.
- A ::after line scales across the removed text first, then the row folds after a 0.3s delay, and the added rows open a beat after that.
- Unchanged lines keep their numbers; changed ones show + or − once After is on.
Accessibility: Before and After are buttons in a labelled group with aria-pressed. Folded rows are aria-hidden, so a screen reader reads only the version on show. Under prefers-reduced-motion the rows switch at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Diff" card.
Visual behavior: A code card with Before and After: removed lines get struck through and fold away, added lines open up in their place, and the gutter marks each change with + or −.
Technique:
- Each line is a grid row whose single track animates between 1fr and 0fr, so it folds to nothing or opens to its natural height without measuring.
- A ::after line scales across the removed text first, then the row folds after a 0.3s delay, and the added rows open a beat after that.
- Unchanged lines keep their numbers; changed ones show + or − once After is on.
Accessibility: Before and After are buttons in a labelled group with aria-pressed. Folded rows are aria-hidden, so a screen reader reads only the version on show. Under prefers-reduced-motion the rows switch at once.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
A code card with Before and After: removed lines get struck through and fold away, added lines open up in their place, and the gutter marks each change with + or −.
Before and After are buttons in a labelled group with aria-pressed. Folded rows are aria-hidden, so a screen reader reads only the version on show. Under prefers-reduced-motion the rows switch at once.