How to Add a Copy to Clipboard Button in MDX with Next.js?

In this article, you'll learn how to easily create a copy to clipboard button using Rehype Pretty Code. We are developing a blog site with Contentlayer #1

Ozan Tekin
lamalab
4 min readAug 7, 2024

--

Lama Lab
Lama Lab

Table of Contents

  • Before You Start
  • Introduction
  • Setting Up the Rehype Pretty Code Plugin
  • Pre Component
  • Code Component
  • Rendering MDX Content
  • Copy Button
  • Adding CopyButton to Pre Component
  • Conclusion

Before You Start

By the end of this series, you’ll have a blog page with dark-light theme, SEO optimization, and enhancements to improve the reading experience. The nextjs-blog-template repository, linked at the end of this article, is a step ahead in this series and provides answers to your UI/UX questions. Check out the repository to get a head start and quickly customize your blog.

Note: Before starting, follow the simple steps in the Contentlayer documentation to complete the basic setup. For the relevant setup, refer to Contentlayer Documentation.

Introduction

Follow these steps to add a “copy to clipboard” button to the code blocks in your Next.js application created with MDX. This feature enhances the user experience and makes code sharing easier.

npm install rehype-pretty-code shiki unist-util-visit

Now, let’s move on to the steps we’ll follow.

Setting Up the Rehype Pretty Code Plugin

Add this section to the makeSource function in your contentlayer.config.ts file.

  mdx: {
rehypePlugins: [
() => (tree) => {
visit(tree, (node) => {
if (node?.type === "element" && node?.tagName === "pre") {
const [codeEl] = node.children;

if (codeEl.tagName !== "code") return;

node.__rawString__ = codeEl.children?.[0].value;
}
});
},
[
// @ts-ignore
rehypePrettyCode,
{
theme: "github-dark",
keepBackground: false,
onVisitLine(node: any) {
if (node.children.length === 0) {
node.children = [{ type: "text", value: " " }];
}
},
},
],
() => (tree) => {
visit(tree, (node) => {
if (node?.type === "element" && node?.tagName === "figure") {
if (!("data-rehype-pretty-code-figure" in node.properties)) {
return;
}

const preElement = node.children.at(-1);
if (preElement.tagName !== "pre") {
return;
}

preElement.properties["__rawString__"] = node.__rawString__;
}
});
},
],
},

Pre Component

interface PreProps extends React.HTMLProps<HTMLPreElement> {
__rawString__?: string;
["data-language"]?: string;
}

export function PreCustom(props: PreProps) {
const {
children,
__rawString__ = "",
["data-language"]: dataLanguage = "Shell",
} = props;

return (
<pre
className="rounded-xl bg-slate-950 relative overflow-hidden p-[0.5rem] shadow-smooth"
{...props}
>
<p className="absolute bottom-0 right-0 capitalize text-xs font-medium bg-slate-700 text-white p-1 rounded-tl-lg">
{dataLanguage}
</p>
{children}
</pre>
);
}

Code Component

import { HTMLAttributes } from "react";
import { cn } from "@/utils/cn";

export const BasicItems = {
code: (props: HTMLAttributes<HTMLElement>) => {
const { className, ...rest } = props;
return (
<code
className={cn(
"rounded-sm bg-slate-950 px-[0.5rem] py-1 font-mono text-sm text-foreground text-pretty leading-relaxed text-white",
className
)}
{...rest}
/>
);
},
};

Rendering MDX Content

import { BasicItems } from "./basic-items";
import { PreCustom } from "./pre-component";

export const MDXComponents = {
pre: PreCustom,
...BasicItems,
};
"use client";

import { useMDXComponent } from "next-contentlayer/hooks";
import { MDXComponents } from "@/components/mdx/components";

interface MdxProps {
code: string;
}

export function Mdx(props: MdxProps) {
const { code } = props;
const Component = useMDXComponent(code);

return <Component components={MDXComponents} />;
}

Copy Button

"use client";

import { Button, ButtonProps } from "@/components/shadcn/button";
import { cn } from "@/utils/cn";
import { Checks, ClipboardText } from "@phosphor-icons/react";
import { useState } from "react";

interface CopyButtonProps extends ButtonProps {
text: string;
className?: string;
}

export function CopyButton({ text, className, ...props }: CopyButtonProps) {
const [isCopied, setIsCopied] = useState(false);

const copy = async () => {
await navigator.clipboard.writeText(text);
setIsCopied(true);

setTimeout(() => {
setIsCopied(false);
}, 700);
};

return (
<Button
size="icon"
className={cn("size-7 !bg-slate-700 !text-white", className)}
disabled={isCopied}
onClick={copy}
aria-label="Copy"
{...props}
>
<span className="sr-only">Copy</span>
{isCopied ? <Checks className="text-green-400" /> : <ClipboardText />}
</Button>
);
}

Adding CopyButton to Pre Component

import { CopyButton } from "./copy-button";

interface PreProps extends React.HTMLProps<HTMLPreElement> {
__rawString__?: string;
["data-language"]?: string;
}

export function PreCustom(props: PreProps) {
const {
children,
__rawString__ = "",
["data-language"]: dataLanguage = "Shell",
} = props;

return (
<pre
className="rounded-xl bg-slate-950 relative overflow-hidden p-[0.5rem] shadow-smooth"
{...props}
>
<p className="absolute bottom-0 right-0 capitalize text-xs font-medium bg-slate-700 text-white p-1 rounded-tl-lg">
{dataLanguage}
</p>
<CopyButton
text={__rawString__}
className="absolute right-1 top-1 shadow-smooth"
/>

{children}
</pre>
);
}

Conclusion

By following these steps, you can easily add a “copy to clipboard” button to your MDX-based site created with Next.js and Contentlayer. This feature will significantly improve code sharing and user experience.

Preview

Feel free to explore the nextjs-blog-template repository for further customization and enhancements to your blog. If you have any questions or need further assistance, please leave a comment below or reach out to us on our GitHub repository. 👋

--

--