6 min read

Copy Code to Clipboard with React Markdown

Extend react-markdown to copy code blocks to clipboard.

ByAmir Ardalan
Like
Share on X
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 Markdown

The 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."

tsx
1// Markdown.tsx 2import ReactMarkdown from 'react-markdown' 3 4<ReactMarkdown 5 components={{ 6 // Map `h1` (`# heading`) to use `h2`s. 7 h1: 'h2', 8 // Rewrite `em`s (`*like so*`) to `i` with a red foreground color. 9 em: ({node, ...props}) => <i style={{color: 'red'}} {...props} /> 10 }} 11/> 12

This 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.

Targeting the Pre Element

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.

tsx
1// Markdown.tsx 2import ReactMarkdown from 'react-markdown' 3 4interface PreNode { 5 node?: any 6 children: Array<object> 7 position: object 8 properties: object 9 tagName: string 10 type: string 11} 12 13<ReactMarkdown 14 components={{ 15 pre: (pre: PreNode) => { 16 return <pre {...pre}></pre> 17 } 18 }} 19/> 20

At 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.

Adding the Copy Button and Click Handler

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.

tsx
1// Markdown.tsx 2 3... 4<ReactMarkdown 5 components={{ 6 pre: (pre: PreNode) => { 7 8 return ( 9 <div className="copyCode"> 10 <button onClick={()=> handleCopyCode()} /> 11 <pre {...pre}></pre> 12 </div> 13 ) 14 } 15 }} 16/> 17

And 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.

typescript
1// Markdown.tsx 2import ReactMarkdown from 'react-markdown' 3 4const styleMarkdown = css({ 5 '.copyCode': { 6 position: 'relative', 7 button: { 8 zIndex: 1, 9 position: 'absolute', 10 top: 13, 11 right: -10, 12 backgroundColor: 'var(--code-highlight)', 13 borderRadius: 5, 14 textTransform: 'uppercase', 15 fontSize: 13, 16 padding: '.1rem .4rem .2rem', 17 color: 'var(--color-bg)', 18 '&:after': { 19 content: '"📋"', 20 }, 21 }, 22 '&.active button:after': { 23 content: '"☑️"' 24 } 25 } 26}) 27 28<ReactMarkdown 29 css={styleMarkdown} 30... 31

Accessing the Raw Code Data for Copying

Let'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.

tsx
1// Markdown.tsx 2 3... 4<ReactMarkdown 5 css={styleMarkdown} 6 components={{ 7 pre: (pre: PreNode) => { 8 const codeChunk = pre.node.children[0].children[0].value 9 10 return ( 11 <div className="copyCode"> 12 <button onClick={()=> handleCopyCode(codeChunk)} /> 13 <pre {...pre}></pre> 14 </div> 15 ) 16 } 17 }} 18/> 19

Setting up State for Our 'Copied' UI Element

Now 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.

tsx
1// Markdown.tsx 2import { useState } from 'react' 3 4... 5<ReactMarkdown 6 css={styleMarkdown} 7 components={{ 8 pre: (pre: PreNode) => { 9 const codeChunk = pre.node.children[0].children[0].value 10 11 const [codeCopied, setCodeCopied] = useState(false) 12 const handleCopyCode = (codeChunk: string) => { 13 setCodeCopied(true) 14 navigator.clipboard.writeText(codeChunk) 15 setTimeout(() => { 16 setCodeCopied(false) 17 }, 5000) 18 } 19 return ( 20 <div className={codeCopied ? 'copyCode active' : 'copyCode'}> 21 <button onClick={()=> handleCopyCode(codeChunk)} /> 22 <pre {...pre}></pre> 23 </div> 24 ) 25 } 26 }} 27/> 28

Putting it all Together

We 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 ✨.

tsx
1// Markdown.tsx 2import { useState } from 'react' 3import ReactMarkdown from 'react-markdown' 4 5const styleMarkdown = css({ 6 '.copyCode': { 7 position: 'relative', 8 button: { 9 zIndex: 1, 10 position: 'absolute', 11 top: 13, 12 right: -10, 13 backgroundColor: 'var(--code-highlight)', 14 borderRadius: 5, 15 textTransform: 'uppercase', 16 fontSize: 13, 17 padding: '.1rem .4rem .2rem', 18 color: 'var(--color-bg)', 19 '&:after': { 20 content: '"📋"', 21 }, 22 }, 23 '&.active button:after': { 24 content: '"☑️"' 25 } 26 } 27}) 28 29interface PreNode { 30 node?: any 31 children: Array<object> 32 position: object 33 properties: object 34 tagName: string 35 type: string 36} 37 38<ReactMarkdown 39 css={styleMarkdown} 40 components={{ 41 pre: (pre: PreNode) => { 42 const codeChunk = pre.node.children[0].children[0].value 43 44 const [codeCopied, setCodeCopied] = useState(false) 45 const handleCopyCode = (codeChunk: string) => { 46 setCodeCopied(true) 47 navigator.clipboard.writeText(codeChunk) 48 setTimeout(() => { 49 setCodeCopied(false) 50 }, 5000) 51 } 52 return ( 53 <div className={codeCopied ? 'copyCode active' : 'copyCode'}> 54 <button onClick={()=> handleCopyCode(codeChunk)} /> 55 <pre {...pre}></pre> 56 </div> 57 ) 58 } 59 }} 60/> 61

Final Thoughts

It'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.✌️

Did you enjoy this post?

Like
© 2024 Amir Ardalan0 views