Next.js + MDX: How to add a copy to clipboard button

Posted on: 2021-08-29

There are several way you can add a "copy to clipboard" button to a code block when using mdx in your next.js app, and the best might be write a remark or rehype plugin. But I did not want to spend hours doing that so I found a much simpler way to proceed...

First, if you added the mdx capability to your next.js project, you should have modified your next.config.js file with the withMDX function. Now the first thing we want to do is use a remark plugin (npm i remark-code-extra) to add an extra div in every generated code block (we will use that div after to make it our Copy to clipboard button) :

const highlight = require('remark-highlight.js'); const codeExtra = require('remark-code-extra'); const withMDX = require('@next/mdx')({ extension: /\.mdx?$/, options: { remarkPlugins: [ highlight, // I'm also using highlight.js to highlight my code, but you don't have to [ codeExtra, { transform: node => ({ before: [ // here we just add a div with the class .copy-to-clipboard before the catual <code/> block : { type: 'element', tagName: 'div', properties: { class: 'copy-to-clipboard' }, } ] }) } ], ], rehypePlugins: [] } }) module.exports = withMDX({ reactStrictMode: true, pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], })

As a result, the generated code block will look like that :

<div class="code-extra"> <div class="copy-to-clipboard"></div> <pre> <code> ... </code> </pre> </div>

Let's style our button a little bit, here with some scss :

.copy-to-clipboard { position: relative; &::after { // This is a "cliboard" icon from bootstrap 5, but you can put anything else you want content: url('data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0iYmkgYmktY2xpcGJvYXJkIiB2aWV3Qm94PSIwIDAgMTYgMTYiPg0KICA8cGF0aCBkPSJNNCAxLjVIM2EyIDIgMCAwIDAtMiAyVjE0YTIgMiAwIDAgMCAyIDJoMTBhMiAyIDAgMCAwIDItMlYzLjVhMiAyIDAgMCAwLTItMmgtMXYxaDFhMSAxIDAgMCAxIDEgMVYxNGExIDEgMCAwIDEtMSAxSDNhMSAxIDAgMCAxLTEtMVYzLjVhMSAxIDAgMCAxIDEtMWgxdi0xeiIvPg0KICA8cGF0aCBkPSJNOS41IDFhLjUuNSAwIDAgMSAuNS41djFhLjUuNSAwIDAgMS0uNS41aC0zYS41LjUgMCAwIDEtLjUtLjV2LTFhLjUuNSAwIDAgMSAuNS0uNWgzem0tMy0xQTEuNSAxLjUgMCAwIDAgNSAxLjV2MUExLjUgMS41IDAgMCAwIDYuNSA0aDNBMS41IDEuNSAwIDAgMCAxMSAyLjV2LTFBMS41IDEuNSAwIDAgMCA5LjUgMGgtM3oiLz4NCjwvc3ZnPg=='); display: flex; align-items: center; justify-content: center; text-align: center; position: absolute; top: 10px; right: 10px; padding: 5px; background: rgba(255, 255, 255, 1); border-radius: 5px; color: red; font-weight: bold; cursor: pointer; width: 36px; height: 36px; } }

Then somewhere in your react tree, in a parent component of the component that will display the mdx (that could be the _app page I guess), you can use the useEffect hook to start the clipboard.js library (that you previously installed by doing npm install clipboard) :

import ClipboardJS from "clipboard"; // ... useEffect(() => { if (typeof window !== 'undefined') { // we dont want to do this when rendering server-side const clipboard = new ClipboardJS('.copy-to-clipboard', {// use the class of the div we created before text: trigger => { // this happens when the user click on our button, "trigger" is the div element next to the code block // so we want to go to the parent, then get the <code> element: const codeElement = trigger?.parentElement?.querySelector('code'); const content = codeElement?.innerText || ''; return content; } }); clipboard.on('success', () => { // if using a toaster library, you can display a success message: // toast.success('Copied to clipboard'); }) return () => { // returning a function in useEffect to cleanup the event handlers etc... try { clipboard.destroy(); } catch (e) {} }; } })

Hope that helps !