Skip to main content
views/

Copy Code to Clipboard with React-Markdown

Amir Ardalan

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} />
  }}
/>
tsx

This 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.tsx
import 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>
}
}}
/>
tsx

At 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>
)
}
}}
/>
tsx

Here 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}
...
typescript

Accessing 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>
)
}
}}
/>
tsx

Adding 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.tsx
import { 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>
)
}
}}
/>
tsx

Putting 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>
      )
    }
  }}
/>
tsx

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