All components
Code Comparison
data-displayA component which compares two code snippets.
responsive · 500px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/code-comparison.jsonUsage
"use client";
import { CodeComparison } from "@/registry/magic-ui/code-comparison";
const beforeCode = `function fetchUser(id) {
return fetch('/api/users/' + id)
.then(function(res) {
return res.json();
})
.then(function(data) {
return data;
})
.catch(function(err) {
console.error(err); // [!code --]
});
}`;
const afterCode = `async function fetchUser(id: string) { // [!code ++]
try { // [!code ++]
const res = await fetch(\`/api/users/\${id}\`); // [!code ++]
if (!res.ok) throw new Error(res.statusText);
return await res.json() as User; // [!code ++]
} catch (err) {
console.error('fetchUser failed:', err);
return null;
}
}`;
export default function Demo() {
return (
<div className="w-full max-w-4xl mx-auto px-4 py-8">
<CodeComparison
beforeCode={beforeCode}
afterCode={afterCode}
language="typescript"
filename="api.ts"
lightTheme="github-light"
darkTheme="github-dark"
highlightColor="#ff3333"
/>
</div>
);
}Component source
"use client"
import { useEffect, useMemo, useState } from "react"
import {
transformerNotationDiff,
transformerNotationFocus,
} from "@shikijs/transformers"
import { FileIcon } from "lucide-react"
import { useTheme } from "next-themes"
import { cn } from "@/lib/utils"
interface CodeComparisonProps {
beforeCode: string
afterCode: string
language: string
filename: string
lightTheme: string
darkTheme: string
highlightColor?: string
}
export function CodeComparison({
beforeCode,
afterCode,
language,
filename,
lightTheme,
darkTheme,
highlightColor = "#ff3333",
}: CodeComparisonProps) {
const { theme, systemTheme } = useTheme()
const [highlightedBefore, setHighlightedBefore] = useState("")
const [highlightedAfter, setHighlightedAfter] = useState("")
const [hasLeftFocus, setHasLeftFocus] = useState(false)
const [hasRightFocus, setHasRightFocus] = useState(false)
const selectedTheme = useMemo(() => {
const currentTheme = theme === "system" ? systemTheme : theme
return currentTheme === "dark" ? darkTheme : lightTheme
}, [theme, systemTheme, darkTheme, lightTheme])
useEffect(() => {
if (highlightedBefore || highlightedAfter) {
setHasLeftFocus(highlightedBefore.includes('class="line focused"'))
setHasRightFocus(highlightedAfter.includes('class="line focused"'))
}
}, [highlightedBefore, highlightedAfter])
useEffect(() => {
async function highlightCode() {
try {
const { codeToHtml } = await import("shiki")
const { transformerNotationHighlight } =
await import("@shikijs/transformers")
const before = await codeToHtml(beforeCode, {
lang: language,
theme: selectedTheme,
transformers: [
transformerNotationHighlight({ matchAlgorithm: "v3" }),
transformerNotationDiff({ matchAlgorithm: "v3" }),
transformerNotationFocus({ matchAlgorithm: "v3" }),
],
})
const after = await codeToHtml(afterCode, {
lang: language,
theme: selectedTheme,
transformers: [
transformerNotationHighlight({ matchAlgorithm: "v3" }),
transformerNotationDiff({ matchAlgorithm: "v3" }),
transformerNotationFocus({ matchAlgorithm: "v3" }),
],
})
setHighlightedBefore(before)
setHighlightedAfter(after)
} catch (error) {
console.error("Error highlighting code:", error)
setHighlightedBefore(`<pre>${beforeCode}</pre>`)
setHighlightedAfter(`<pre>${afterCode}</pre>`)
}
}
highlightCode()
}, [beforeCode, afterCode, language, selectedTheme])
const renderCode = (code: string, highlighted: string) => {
if (highlighted) {
return (
<div
style={{ "--highlight-color": highlightColor } as React.CSSProperties}
className={cn(
"bg-background h-full w-full overflow-auto font-mono text-xs",
"[&>pre]:h-full [&>pre]:w-screen! [&>pre]:py-2",
"[&>pre>code]:inline-block! [&>pre>code]:w-full!",
"[&>pre>code>span]:inline-block! [&>pre>code>span]:w-full [&>pre>code>span]:px-4 [&>pre>code>span]:py-0.5",
"[&>pre>code>.highlighted]:inline-block [&>pre>code>.highlighted]:w-full [&>pre>code>.highlighted]:bg-(--highlight-color)!",
"group-hover/left:[&>pre>code>:not(.focused)]:opacity-100! group-hover/left:[&>pre>code>:not(.focused)]:blur-none!",
"group-hover/right:[&>pre>code>:not(.focused)]:opacity-100! group-hover/right:[&>pre>code>:not(.focused)]:blur-none!",
"[&>pre>code>.add]:bg-[rgba(16,185,129,.16)] [&>pre>code>.remove]:bg-[rgba(244,63,94,.16)]",
"group-hover/left:[&>pre>code>:not(.focused)]:transition-all group-hover/left:[&>pre>code>:not(.focused)]:duration-300",
"group-hover/right:[&>pre>code>:not(.focused)]:transition-all group-hover/right:[&>pre>code>:not(.focused)]:duration-300"
)}
dangerouslySetInnerHTML={{ __html: highlighted }}
/>
)
} else {
return (
<pre className="bg-background text-foreground h-full overflow-auto p-4 font-mono text-xs break-all">
{code}
</pre>
)
}
}
return (
<div className="mx-auto w-full max-w-5xl">
<div className="group border-border relative w-full overflow-hidden rounded-md border">
<div className="relative grid md:grid-cols-2">
<div
className={cn(
"leftside group/left border-primary/20 md:border-r",
hasLeftFocus &&
"[&>div>pre>code>:not(.focused)]:opacity-50! [&>div>pre>code>:not(.focused)]:blur-[0.095rem]!",
"[&>div>pre>code>:not(.focused)]:transition-all [&>div>pre>code>:not(.focused)]:duration-300"
)}
>
<div className="border-primary/20 bg-accent text-foreground flex items-center border-b p-2 text-sm">
<FileIcon className="mr-2 h-4 w-4" />
{filename}
<span className="ml-auto hidden md:block">before</span>
</div>
{renderCode(beforeCode, highlightedBefore)}
</div>
<div
className={cn(
"rightside group/right border-primary/20 border-t md:border-t-0",
hasRightFocus &&
"[&>div>pre>code>:not(.focused)]:opacity-50! [&>div>pre>code>:not(.focused)]:blur-[0.095rem]!",
"[&>div>pre>code>:not(.focused)]:transition-all [&>div>pre>code>:not(.focused)]:duration-300"
)}
>
<div className="border-primary/20 bg-accent text-foreground flex items-center border-b p-2 text-sm">
<FileIcon className="mr-2 h-4 w-4" />
{filename}
<span className="ml-auto hidden md:block">after</span>
</div>
{renderCode(afterCode, highlightedAfter)}
</div>
</div>
<div className="border-primary/20 bg-accent text-foreground absolute top-1/2 left-1/2 hidden h-8 w-8 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-md border text-xs md:flex">
VS
</div>
</div>
</div>
)
}Dependencies
shikinext-themes
Source: Magic UI