Copy Code to Clipboard with React-Markdown
In this guide, I will show you how to add a copy code to clipboard feature to react-markdown using TypeScript. Once again, we will use its custom component functionality.
Introduction to Custom Components in React Markdown
The official documentation provides the following example of overriding elements rendered from Markdown:
"The keys in components are HTML equivalents for the things you write with markdown. Every component will receive a
node(Object). This is the original hast element being turned into a React element."
// Markdown.tsx
import ReactMarkdown from 'react-markdown'
<ReactMarkdown
components={{
// Map `h1` (`# heading`) to use `h2`s.
h1: 'h2',
// Rewrite `em`s (`*like so*`) to `i` with a red foreground color.
em: ({ node, ...props }) => <i style={{ color: 'red' }} {...props} />
}}
/>
tsxThis is a powerful feature. It allows us to target any HTML element rendered from Markdown and replace or modify it.
For this guide, we will target the pre element that wraps each code block.
Targeting the pre Element
Let's begin by overriding the <pre> element while passing through its existing props. This preserves the current behavior of our code blocks while giving us a place to add the copy button.
We will also create a TypeScript interface for the custom node object.
// Markdown.tsximport ReactMarkdown from 'react-markdown'interface PreNode { node?: any children: Array<object> position: object properties: object tagName: string type: string}<ReactMarkdown components={{ pre: (pre: PreNode) => { return <pre {...pre}></pre> } }}/>tsxAt this point, everything should look the same. We are replacing the existing <pre> element with an equivalent one, but we now have a place to add our custom functionality.
Adding the Copy Button and Click Handler
Next, we will wrap the <pre> element in a div. This gives us a positioned container that we can use to place the copy button over the code block.
// Markdown.tsx...<ReactMarkdown components={{ pre: (pre: PreNode) => { return ( <div className="copyCode"> <button onClick={() => handleCopyCode()} /> <pre {...pre}></pre> </div> ) } }}/>tsxHere is the corresponding CSS. This example uses Emotion, but the styles can be adapted to whichever CSS approach you prefer.
// Markdown.tsx
import ReactMarkdown from 'react-markdown'
const styleMarkdown = css({
'.copyCode': {
position: 'relative',
button: {
zIndex: 1,
position: 'absolute',
top: 13,
right: -10,
backgroundColor: 'var(--code-highlight)',
borderRadius: 5,
textTransform: 'uppercase',
fontSize: 13,
padding: '.1rem .4rem .2rem',
color: 'var(--color-bg)',
'&:after': {
content: '"📋"',
},
},
'&.active button:after': {
content: '"☑️"'
}
}
})
<ReactMarkdown
css={styleMarkdown}
...
typescriptAccessing the Raw Code for Copying
Next, we create a variable that stores the raw text from the code block.
Using standard dot notation, we can access the original string from the underlying Markdown syntax tree. This value, along with other useful data, is available through the node object created from the original hast element.
// Markdown.tsx...<ReactMarkdown css={styleMarkdown} components={{ pre: (pre: PreNode) => { const codeChunk = pre.node.children[0].children[0].value return ( <div className="copyCode"> <button onClick={() => handleCopyCode(codeChunk)} /> <pre {...pre}></pre> </div> ) } }}/>tsxAdding State for the Copied Indicator
Now, let's import useState and add a temporary visual state that confirms the code has been copied.
We will initially set codeCopied to false. When the button is clicked, we copy the codeChunk to the clipboard and update the state to true.
Finally, we use setTimeout to reset the state after five seconds. This provides clear feedback that the copy action was successful.
// Markdown.tsximport { useState } from 'react'...<ReactMarkdown css={styleMarkdown} components={{ pre: (pre: PreNode) => { const codeChunk = pre.node.children[0].children[0].value const [codeCopied, setCodeCopied] = useState(false) const handleCopyCode = (codeChunk: string) => { setCodeCopied(true) navigator.clipboard.writeText(codeChunk) setTimeout(() => { setCodeCopied(false) }, 5000) } return ( <div className={codeCopied ? 'copyCode active' : 'copyCode'}> <button onClick={() => handleCopyCode(codeChunk)} /> <pre {...pre}></pre> </div> ) } }}/>tsxPutting It All Together
We now have a button on each code block that copies its raw contents to the clipboard. We are also using state to provide visual feedback when the action succeeds.
// Markdown.tsx
import { useState } from 'react'
import ReactMarkdown from 'react-markdown'
const styleMarkdown = css({
'.copyCode': {
position: 'relative',
button: {
zIndex: 1,
position: 'absolute',
top: 13,
right: -10,
backgroundColor: 'var(--code-highlight)',
borderRadius: 5,
textTransform: 'uppercase',
fontSize: 13,
padding: '.1rem .4rem .2rem',
color: 'var(--color-bg)',
'&:after': {
content: '"📋"',
},
},
'&.active button:after': {
content: '"☑️"'
}
}
})
interface PreNode {
node?: any
children: Array<object>
position: object
properties: object
tagName: string
type: string
}
<ReactMarkdown
css={styleMarkdown}
components={{
pre: (pre: PreNode) => {
const codeChunk = pre.node.children[0].children[0].value
const [codeCopied, setCodeCopied] = useState(false)
const handleCopyCode = (codeChunk: string) => {
setCodeCopied(true)
navigator.clipboard.writeText(codeChunk)
setTimeout(() => {
setCodeCopied(false)
}, 5000)
}
return (
<div className={codeCopied ? 'copyCode active' : 'copyCode'}>
<button onClick={() => handleCopyCode(codeChunk)} />
<pre {...pre}></pre>
</div>
)
}
}}
/>
tsxFinal Thoughts
Custom component overrides make react-markdown straightforward to extend.
Unified also provides many existing Rehype and Remark plugins that may already support the behavior you need. At the time of writing, however, there was no plugin specifically for copying Markdown code blocks to the clipboard.
In this case, building the feature directly was simple and avoided adding another dependency to the project.
In addition to react-syntax-highlighter, my custom Next/Image component, and this copy-to-clipboard feature, I am also using rehype-slug, rehype-auto-link-headings, rehype-raw, and remark-gfm.