6 min read
— views•Copy Code to Clipboard with React-Markdown
This post is quite old and no longer being updated. The following information may no longer work the with the latest version of the tools, libraries, frameworks, or best-practices discussed.
In this guide, I will show you how to implement a copy code to clipboard feature with react-markdown using TypeScript. Once again, we will be utilizing the custom component feature.
#Introduction to Custom Components with React MarkdownThe official documentation shows this example for overriding various elements in your 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 an incredibly powerful feature. It allows us to select any HTML element rendered from markdown and modify it.
For the sake of this guide, we are going to be targeting the pre
element which wraps code blocks.
Let's start by setting up a <pre>
element and passing it the existing props to keep the existing functionality of our code blocks. This will set us up to be able to add our button to copy the code. We'll also create a type interface for our 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 overriding the existing <pre>
elements with the same thing that was rendering before. But now we have a place to put our custom code.
Here we will add a wrapping div around the <pre>
tag. This will give us a way to absolutely position 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> ) } }}/>
tsxAnd the corresponding CSS, for this guide, I am using Emotion, but it should be simple enough to refactor to whatever type of CSS you're using.
// 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}
...
typescriptLet's create a variable that stores the raw data for this code block. Using standard dot notation, we're accessing the raw string from the underlying markdown. This string, along with other useful data, is passed to the node
object from the base 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> ) } }}/>
tsxNow let's import useState
and set up our state for styling the button to notify the user that the code has been copied to their clipboard. Note that the className
is being refactored to support a ternary linked to our state.
We'll initially set the state to false
, and then true
once the button is clicked along with our code to copy the codeChunk
to the clipboard.
Lastly, we will utilize a setTimeout
to display the "copied" styling for 5 seconds. This simple feedback makes it clear that the code was, in fact, copied.
// 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> ) } }}/>
tsxWe now have a button on each code block that copies the raw code data to clipboard. We're also utilizing a state management hook to visually notify users that the action was successful ✨.
// 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>
)
}
}}
/>
tsxIt's quite simple to extend react-markdown
with custom component overrides. Unified provides a lot of out of the box Rehype and Remark plugins which may suit your needs, but there is currently no plugin for copying the markdown code to clipboard. I'm glad I didn't have the temptation to add a dependency to the project when making my own feature was so easy (and fun!).
In addition to react-syntax-highlighter
, my custom Next/Image component, and this copy to clipboard feature, I am using rehype-slug, rehype-auto-link-headings, rehype-raw, and remark-gfm. I may write some guides on a few of those plugins and my configurations in the future.
As always, you can find me on Twitter if you have any questions.✌️
Enjoy this post? Like and share!