Gatsby: Copy code button with confetti effect for MDX blog

Create a copy to clipboard button with confetti effect for Gatsby blog with MDX support and react-prism-renderer module
gatsby
react
Written on August 17, 2020 ·
7 min read

TL;DR

We'll build a versatile Copy to clipboard button for a Gatsby MDX blog. Because code is boring, we'll add a little bit of flair with a confetti explosion

A quick preview of what we'll build in this tutorial.

Before starting, you can check the live example.

Installation

We'll create a new Gatsby project from this starter template that has build-in support for a MDX blog and the prism-react-renderer module.

bash

gatsby new gatsby-starter-blog-mdx https://github.com/hagnerd/gatsby-starter-blog-mdx

Understand the basics of MDX

The entry point for MDX is the MDXProvider component that handles internally the mapping of components to MDX. Also, it has a very important prop.

The components prop is an object that allows you to override the default component for each HTML element (here is a list for them) or even provide your own as shortcodes.

The Gatsby template uses the MDXProvider inside the wrapRootElement browser API.

The wrapRootElement browser API is useful to set up any Provider components that will wrap your application.

Below you see the wrap-root-element.js file that sets up the MDXProvider and overrides the pre element with a custom Code component.

jsx

import React from "react"
import { MDXProvider } from "@mdx-js/react"
import { Code } from "./src/components/code"
import { preToCodeBlock } from "mdx-utils"
const components = {
pre: preProps => {
const props = preToCodeBlock(preProps)
if (props) {
return <Code {...props} />
} else {
return <pre {...preProps} />
}
},
}
export const wrapRootElement = ({ element }) => (
<MDXProvider components={components}>{element}</MDXProvider>
)

Then, our wrapper is added to both gatsby-browser and gatsby-ssr.js files to render the root element of the Gatsby app.

jsx

import { wrapRootElement as wrap } from "./wrap-root-element"
export const wrapRootElement = wrap

Adjust the custom code component

The custom Code component lives in the src/components/code.js file and utilizes the prism-react-renderer. The prism-react-renderer is the perfect way to render some extra UI with your Prismjs-highlighted code.

The library tokenises code using Prism and provides a small render-props-driven component to quickly render it out into React.

The default code.js is the following:

jsx

import React from "react"
import { render } from "react-dom"
import Highlight, { defaultProps } from "prism-react-renderer"
import { LiveProvider, LiveEditor, LiveError, LivePreview } from "react-live"
export const Code = ({ codeString, language, ...props }) => {
if (props["react-live"]) {
return (
<LiveProvider code={codeString} noInline={true}>
<LiveEditor />
<LiveError />
<LivePreview />
</LiveProvider>
)
} else {
return (
<Highlight {...defaultProps} code={codeString} language={language}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={style}>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
)
}
}

The magic happens inside Highlight component. The pre element renders the code wrapper and the render-prop functions provide the necessary props for each line and token/word.

The copy to clipboard button will live inside the pre element.

Create the copy button

The copy button will be placed at the top right corner of the code section.

To achieve that positioning, we'll set the position of the pre element to relative and add a little bit of padding.

jsx

<pre
className={className}
style={{
...style,
padding: "2rem",
position: "relative",
}}
>
...
</pre>

The Button component is a simple button element that is placed with position: absolute:

jsx

const Button = props => (
<button
style={{
position: "absolute",
top: 0,
right: 0,
border: "none",
boxShadow: "none",
textDecoration: "none",
margin: "8px",
padding: "8px 12px",
background: "#E2E8F022",
color: "white",
borderRadius: "8px",
cursor: "pointer",
color: "#E2E8F0",
fontSize: "14px",
fontFamily: "sans-serif",
lineHeight: "1",
}}
{...props}
/>
)

For a better UX, your users should be informed about the outcome of their actions. So it's a nice extra feature to toggle the button's text once the code is copied.

Then, we have to set a React hook state variable isCopied.

jsx

const [isCopied, setIsCopied] = React.useState(false)

The isCopied variable gets true when the user clicks the copy button and resets to false after a specific amount of time (eg. 3 seconds).

jsx

<Button
onClick={() => {
copyToClipboard(codeString)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 3000)
}}
>
{isCopied ? "🎉 Copied!" : "Copy"}
</Button>

The copyToClipboard is our core functionality here. I've re-used a function from this article.

js

const copyToClipboard = str => {
const el = document.createElement("textarea")
el.value = str
el.setAttribute("readonly", "")
el.style.position = "absolute"
el.style.left = "-9999px"
document.body.appendChild(el)
el.select()
document.execCommand("copy")
document.body.removeChild(el)
}

The final code component

By now, we have the custom Code component, the copyToClipboard function, and the Button component. Then, the final code component is the following:

jsx

export const Code = ({ codeString, children, language, ...props }) => {
const [isCopied, setIsCopied] = React.useState(false)
if (props["react-live"]) {
return (
<LiveProvider code={codeString} noInline={true}>
<LiveEditor />
<LiveError />
<LivePreview />
</LiveProvider>
)
} else {
return (
<Highlight
{...defaultProps}
code={codeString}
language={language}
theme={dracula}
>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre
className={className}
style={{
...style,
padding: "2rem",
position: "relative",
}}
>
<Button
onClick={() => {
copyToClipboard(codeString)
setIsCopied(true)
setTimeout(() => setIsCopied(false), 3000)
}}
>
{isCopied ? "🎉 Copied!" : "Copy"}
</Button>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })} style={style}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
)
}
}

The Confetti party

The code works fine. The copy to clipboard functionality is perfect. But we still miss the flair!

There is a known secret in the frontend development community!

Shhh GIF

Everything is better with a little confetti

It's so useless but we're gonna congratulate our readers with a confetti rain.

To bring this into life, we'll have to install the React dependency react-dom-confetti.

bash

yarn add react-dom-confetti

The configuration is pretty straightforward. It's just a JSON object with a couple of options:

js

const config = {
angle: 90,
spread: 360,
startVelocity: 40,
elementCount: 70,
dragFriction: 0.12,
duration: 3000,
stagger: 3,
width: "10px",
height: "10px",
perspective: "500px",
colors: ["#a864fd", "#29cdff", "#78ff44", "#ff718d", "#fdff6a"],
}

The next step is to add the Confetti component. This component explodes to a confetti rain every time the prop active is true.

Then, we just have to pass the isCopied variable to make it explode in our example. Easy, huh?

jsx

<Confetti active={isCopied} config={config} />

To place the Confetti component, we can use the same positioning trick as before. Because we want to fire the explosion in front of the button.

We'll setup a Wrapper component for the Confetti and Highlight components with the CSS attribute position: relative. Then, we'll wrap the Confetti component with the ConfettiWrapper which is placed absolutely at the top right corner.

jsx

<Wrapper>
<Highlight>...</Highlight>
<ConfettiWrapper>
<Confetti active={isCopied} config={config} />
</ConfettiWrapper>
</Wrapper>

And the code for the two wrappers:

jsx

const Wrapper = props => <div style={{ position: "relative" }} {...props} />
const ConfettiWrapper = props => (
<div style={{ position: "absolute", top: 0, right: 0 }} {...props} />
)

That's all folks

Clone the Github repository and don't forget to show me your creation by tagging me, @d__raptis on Twitter 💪

If you liked this post, you can follow me on Twitter where I share daily tips about coding, design and bootstrapping micro-startups.

Join my weekly newsletter

It’s one email a week with everything interesting I’ve built or found, plus new articles and apps.