BlogWorkAboutFree ToolsNewsletter
BlogWorkAboutFree ToolsNewsletter
All articles

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

Written by Jim Raptis

Aug 17, 2020 • 6 min

Gatsby: Copy code button with confetti effect for MDX blog
📖
TLDR: 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 an MDX blog and the prism-react-renderer module.

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.

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.

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:

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.

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

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}
  />
)

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)

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>

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

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

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!

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 to life, we'll have to install the React dependency react-dom-confetti.

    yarn add react-dom-confetti

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"],
}

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} />

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>

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} />
)

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 bootstraping, product development, and UI/UX design tips.

More articles like this:

Jim Raptis

© 2024

RECOMMENDED