DEV Community

Cover image for Gatsby MDX: Copy code button with confetti effect
Jim Raptis
Jim Raptis

Posted on • Updated on • Originally published at jimraptis.com

Gatsby MDX: Copy code button with confetti effect

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 built-in support for an MDX blog and the prism-react-renderer module.

gatsby new gatsby-starter-blog-mdx https://github.com/hagnerd/gatsby-starter-blog-mdx
Enter fullscreen mode Exit fullscreen mode

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.

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>
)
Enter fullscreen mode Exit fullscreen mode

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

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

export const wrapRootElement = wrap
Enter fullscreen mode Exit fullscreen mode

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 tokenizes 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:

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

The magic happens inside the 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.

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

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

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}
  />
)
Enter fullscreen mode Exit fullscreen mode

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.

const [isCopied, setIsCopied] = React.useState(false)
Enter fullscreen mode Exit fullscreen mode

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

<Button
  onClick={() => {
    copyToClipboard(codeString)
    setIsCopied(true)
    setTimeout(() => setIsCopied(false), 3000)
  }}
>
  {isCopied ? "πŸŽ‰ Copied!" : "Copy"}
</Button>
Enter fullscreen mode Exit fullscreen mode

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

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)
}
Enter fullscreen mode Exit fullscreen mode

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:

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

    yarn add react-dom-confetti
Enter fullscreen mode Exit fullscreen mode

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

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"],
}
Enter fullscreen mode Exit fullscreen mode

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?

<Confetti active={isCopied} config={config} />
Enter fullscreen mode Exit fullscreen mode

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 set up 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.

<Wrapper>
  <Highlight>...</Highlight>

  <ConfettiWrapper>
    <Confetti active={isCopied} config={config} />
  </ConfettiWrapper>
</Wrapper>
Enter fullscreen mode Exit fullscreen mode

And the code for the two wrappers:

const Wrapper = props => <div style={{ position: "relative" }} {...props} />

const ConfettiWrapper = props => (
  <div style={{ position: "absolute", top: 0, right: 0 }} {...props} />
)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)