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