feat: add copy to clipboard functionality for code blocks (#934)

## 🚀 Feature: Copy to Clipboard for Code Blocks

### What's Changed
- Added a copy button to all code blocks that allows users to easily
copy code snippets
- Implemented visual feedback showing a checkmark when code is
successfully copied
- Copy button automatically reverts back after 2 seconds

### Technical Details
- Uses `navigator.clipboard.writeText()` for modern clipboard API
- Positioned copy button in the top-right corner alongside language
label
- Maintains existing code highlighting functionality

### UI/UX Improvements
- Clean, minimal copy button design that doesn't interfere with code
readability
- Clear visual feedback with copy and check icon transition
- Consistent styling with existing theme system

### Video



https://github.com/user-attachments/assets/8f388217-da8a-422e-9087-42cce8df68ad

---------

Co-authored-by: Will Chen <willchen90@gmail.com>
This commit is contained in:
Adeniji Adekunle James
2025-08-16 00:52:37 +01:00
committed by GitHub
parent b06f658fc5
commit e554fd962b

View File

@@ -1,9 +1,16 @@
import React, { useEffect, useRef, memo, type ReactNode } from "react";
import React, {
useState,
useEffect,
useRef,
memo,
type ReactNode,
} from "react";
import { isInlineCode, useShikiHighlighter } from "react-shiki";
import github from "@shikijs/themes/github-light-default";
import githubDark from "@shikijs/themes/github-dark-default";
import type { Element as HastElement } from "hast";
import { useTheme } from "../../contexts/ThemeContext";
import { Copy, Check } from "lucide-react";
interface CodeHighlightProps {
className?: string | undefined;
@@ -16,6 +23,13 @@ export const CodeHighlight = memo(
const code = String(children).trim();
const language = className?.match(/language-(\w+)/)?.[1];
const isInline = node ? isInlineCode(node) : false;
//handle copying code to clipboard with transition effect
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000); // revert after 2s
};
const { isDarkMode } = useTheme();
@@ -44,15 +58,24 @@ export const CodeHighlight = memo(
return !isInline ? (
<div
className="shiki not-prose relative [&_pre]:overflow-auto
[&_pre]:rounded-lg [&_pre]:px-6 [&_pre]:py-5"
[&_pre]:rounded-lg [&_pre]:px-6 [&_pre]:py-7"
>
{language ? (
<span
className="absolute right-3 top-2 text-xs tracking-tighter
text-muted-foreground/85"
>
{language}
</span>
<div className="absolute top-2 left-2 right-2 text-xs flex justify-between">
<span className="tracking-tighter text-muted-foreground/85">
{language}
</span>
{code && (
<button
className="mr-2 flex items-center text-xs cursor-pointer"
onClick={handleCopy}
type="button"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
<span className="ml-1">{copied ? "Copied" : "Copy"}</span>
</button>
)}
</div>
) : null}
{displayedCode}
</div>