I found a few examples online for creating a code block with syntax highlighting and a copy-to-clipboard button, but I couldn’t get them to work straight away. So I thought, “It can’t be that hard to make this myself.” After some trial and error, I came up with these two components: CodeBlock
and CopyToClipboard
. Here’s how they work together and what they do:
The CodeBlock
component is responsible for:
CopyToClipboard
button for easy copying of the code.code
: The code snippet to display.language
: The programming language of the code snippet for syntax highlighting.hljs.highlightElement
method is called on the code
element to apply syntax highlighting.felipec.css
in this case) to style the highlighted code.html-react-parser
library is used to safely parse HTML content in the code
string, ensuring proper rendering of any HTML tags.useRef
hook is used to get a reference to the <code>
element.useEffect
hook ensures the highlighting runs whenever the language
prop changes.<div>
containing the code block is styled with padding, margins, and a rounded-lg
border for aesthetics.const CodeBlock = ({ code, language }: Props) => {
const codeRef = useRef<HTMLElement>(null); // Ref to access the <code> element
useEffect(() => {
if (codeRef.current) {
try {
hljs.highlightElement(codeRef.current); // Apply syntax highlighting
} catch (error) {
console.error(`Error highlighting code with language "${language}":`, error);
}
}
}, [language]); // Re-run highlighting if the language changes
return (
<div className='rounded-lg mr-11 ml-6 mt-6 mb-6 relative'>
{/* Copy-to-clipboard button */}
<CopyToClipboard content={parse(code) as string}></CopyToClipboard>
<pre>
<code ref={codeRef} className={`language-${language} rounded-2xl min-h-28`}>
{parse(code)} {/* Render the parsed code */}
</code>
</pre>
</div>
);
};
CopyToClipboard
ComponentThe CopyToClipboard
component handles the logic for:
content
: The text content to copy to the clipboard.navigator.clipboard.writeText
method is used to copy the content
to the user’s clipboard.isCopied
state tracks whether the content has been successfully copied.setTimeout
resets isCopied
to false
after 1 second, so the “copied!” message disappears.const CopyToClipboard = ({ content }: CopyProp) => {
const [isCopied, setIsCopied] = useState(false); // Tracks if the content is copied
const copy = async (content: string) => {
try {
await navigator.clipboard.writeText(content); // Copy content to clipboard
setIsCopied(true); // Show "copied!" feedback
setTimeout(() => {
setIsCopied(false); // Reset feedback after 1 second
}, 1000);
} catch (err) {
console.error("Failed to copy text: ", err); // Handle copy errors
}
};
return (
<button
className={`w-6 h-6 absolute right-6 top-5 text-xs ${isCopied ? "text-zinc-400" : "text-zinc-500"}`}
onClick={() => copy(content)} // Call copy function on click
>
{/* SVG icon for clipboard */}
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="..." stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
{isCopied ? "copied!" : "copy"} {/* Display feedback or default "copy" */}
</button>
);
};
CodeBlock
component renders a code snippet with syntax highlighting using Highlight.js.CopyToClipboard
component is added to the CodeBlock
, positioned as a floating button on the top-right corner.content
) to the clipboard and briefly shows a “copied!” message.