💻
PostsMeContact

Beautifully Rich Markdown Code Snippets

December 16, 2021📚 9 min readTweet this post
Swerved Building
Photo By Elliot Paris

I've recently spent a good amount of time (probably too much) customizing the code snippets on my blog here to my exact liking. There's much more I want to do, but I'm really proud of how they turned out and I want to share with you how I created them!

Its my humble opinion that good code snippets are some of the most beautiful pieces of user interface on the web.

When I think of a good dev blog or good dev docs; I think of beautiful, rich, elegant code snippets that show snippets of code in context with the post. Done well, code snippets can really enhance the experience of reading development content. Some of my favorite code snippets are on sites like Gatsby, Stripe, and Josh W Comeau's Blog.

Markdown Code Snippets

Even more beautiful to me are code snippets in markdown because you can take this basic syntax —

```js
function hi() {
  console.log("hello");
}
```

— and transform it into this beautiful and feature rich code snippet for your readers!

js
1function hi() {
2 console.log("hello");
3}

This is what makes Markdown so powerful! Writing minimal syntax, with intent, resulting in beautiful experiences.

My Setup

I'm gunna show you how I was able to make these custom code snippets on my blog using Gatsby, MDX, and prism-react-renderer.

In this post I'm going to assume you know the basics of React. Things like rendering components, props, state, hooks.

Though I'm using this specific stack, the pieces can be adapted to use various combonations of technologies such as —

  • NextJS
  • Tailwind CSS
  • remark
  • Styled-components
  • highlight.js

Gatsby + MDX

MDX is the engine that translates plain markdown into awesome web markup. MDX is properly known for allowing a new flavor of markdown which you can render JSX within! Hence the name MD(X). We'll not be using that feature today. Instead we'll focus on the markdown rendering capabilites of MDX.

Install MDX by running —

shell
yarn add @mdx-js/react @mdx-js/mdx

MDX transforms tripple backtick's (```) into <pre /> tags on our webpage. So we're going to do some setup to prepare Gatsby and MDX for customization of the <pre /> tags that MDX renders. We do that by rendering our own <MDXProvider /> component in Gatsby.

I like to do this by creating a few encapsulated components. First of which is <MDXProvider />

jsx
src/components/MDXProvider.js
1import React from "react";
2import { MDXProvider as BaseMDXProvider } from "@mdx-js/react";
3
4// This is what we'll use later to customize our code snippets!
5const components = {};
6
7function MDXProvider({ children }) {
8 return <BaseMDXProvider components={components}>{children}</BaseMDXProvider>;
9}
10
11export default MDXProvider;

Then we create <RootWrapper /> to use <MDXProvider />

jsx
src/components/RootWrapper.js
1import React from "react";
2import MDXProvider from "./MDXProvider";
3
4function RootWrapper({ children }) {
5 return <MDXProvider>{children}</MDXProvider>;
6}
7
8export default RootWrapper;

Finally, we need to modify wrapRootElement in both gatsby-browser.js and gatsby-ssr.js so that our entire app is wrapped with <RootWrapper />

jsx
gatsby-browser.js,gatsby-ssr.js
1import React from "react";
2import RootWrapper from "./src/components/RootWrapper";
3
4export const wrapRootElement = ({ element }) => (
5 <RootWrapper>{element}</RootWrapper>
6);

Now we are setup to deeply customize the <pre /> tags rendered by MDX!

PrismJS and prism-react-renderer

PrismJS is a syntax highlighting library for code snippets on the web. It works by taking in code and wrapping the pieces of the code in HTML tags with semantic class names so we can style things like variables, function names, booleans, numbers, etc.

prism-react-renderer wraps PrismJS and gives us control to render each element as we see fit! This is the power that gives us the ability to create such rich and beautiful code snippets.

Install prism-react-render by running —

shell
yarn add prism-react-renderer

Now we're going to create our rich code snippet! Create a new component, <CodeSnippet />, that looks like this —

jsx
src/components/CodeSnippet.js
1import React, { useState } from "react";
2import Highlight, { defaultProps } from "prism-react-renderer";
3import "./CodeSnippet.css";
4
5function CodeSnippet({ children, lang = "markup" }) {
6 return (
7 <Highlight {...defaultProps} code={children.trim()} language={lang}>
8 {({ className, style, tokens, getLineProps, getTokenProps }) => (
9 <div className="code-wrapper">
10 <div className="code-language-badge">{lang}</div>
11 <pre className={`code-preformatted ${className}`} style={style}>
12 <CopyButton codeString={codeString} />
13 <code className="code-snippet">
14 {tokens.map((line, i) => (
15 <div
16 {...getLineProps({
17 line,
18 key: i,
19 className: "code-snippet-line-wrapper",
20 })}
21 >
22 <div className="code-snippet-line-number">{i + 1}</div>
23 <div className="code-snippet-line">
24 {line.map((token, key) => (
25 <span {...getTokenProps({ token, key })} />
26 ))}
27 </div>
28 </div>
29 ))}
30 </code>
31 </pre>
32 </div>
33 )}
34 </Highlight>
35 );
36}
37
38function CopyButton({ codeString }) {
39 const [isCopied, setIsCopied] = useState(false);
40
41 function copyToClipboard(str) {
42 const el = document.createElement("textarea");
43 el.value = str;
44 el.setAttribute("readonly", "");
45 el.style.position = "absolute";
46 el.style.left = "-9999px";
47 document.body.appendChild(el);
48 el.select();
49 document.execCommand("copy");
50 document.body.removeChild(el);
51 }
52
53 return (
54 <button
55 className="code-snippet-copy-button"
56 onClick={() => {
57 copyToClipboard(codeString);
58 setIsCopied(true);
59 setTimeout(() => setIsCopied(false), 3000);
60 }}
61 >
62 {isCopied ? "Copied!" : "Copy"}
63 </button>
64 );
65}
66export default CodeSnippet;

Let's pause and read through this code for a bit. There's a lot to unpack.

First off we have the <CodeSnippet /> component which does all the magic of rendering each code snippet. This is a very nested and complex component. However, this complexity allows us to customize each intricate detail because we control how each element is rendered with react.

In <CodeSnippet /> we have our <Highlight /> component from prism-react-renderer. If you're curious about the inner workings here, refer to the prism-react-renderer README.

Then we have our <CopyButton /> component which is placed within the code snippet allowing the user to copy our code snippet which is very useful for blog posts like this one 😉.

Finally, here's the accompanying styles that make our code snippet beautiful —

css
src/components/CodeSnippet.css
1.code-wrapper {
2 position: relative;
3}
4
5.code-language-badge {
6 position: absolute;
7 left: 1.5rem;
8 text-transform: uppercase;
9 background-color: rgb(91, 33, 182);
10 padding: 0.25rem 0.5rem;
11 font-size: 0.75rem;
12 line-height: 1rem;
13 border-bottom-right-radius: 0.375rem;
14 border-bottom-left-radius: 0.375rem;
15}
16
17.code-preformatted {
18 overflow: scroll;
19 font-size: 0.875rem;
20 line-height: 1.25rem;
21 border-radius: 0.25rem;
22 padding-top: 2.5rem;
23 padding-bottom: 1.5rem;
24}
25
26.code-snippet {
27 display: inline-block;
28}
29
30.code-snippet-line-wrapper {
31 position: relative;
32 display: flex;
33}
34
35.code-snippet-line-number {
36 position: sticky;
37 display: inline-block;
38 left: 0;
39 text-align: right;
40 padding-right: 0.75rem;
41 background-color: rgb(244, 244, 245);
42 user-select: none;
43 width: 3rem;
44}
45
46.code-snippet-line {
47 display: inline;
48}
49
50.code-snippet-copy-button {
51 border-radius: 0.25rem;
52 position: absolute;
53 top: 0.5rem;
54 right: 0.5rem;
55 padding: 0.5rem;
56 font-size: 0.875rem;
57 line-height: 1.25rem;
58}

Revisiting our <MDXProvider /> component, we're going to wire up our <CodeSnippet /> component to MDX when it renders <pre /> tags.

jsx
src/components/MDXProvider.js
1import React from "react";
2import { MDXProvider as BaseMDXProvider } from "@mdx-js/react";
3import CodeSnippet from "./CodeSnippet";
4
5function transformCode({ children, className, ...props }) {
6 // Parse lang from className. i.e. className is "language-jsx"
7 const lang = className && className.split("-")[1];
8
9 return (
10 <CodeSnippet lang={lang} {...props}>
11 {children}
12 </CodeSnippet>
13 );
14}
15
16function getCodeChild(children) {
17 const childrenArray = React.Children.toArray(children);
18 if (childrenArray.length !== 1) return null;
19 const [firstChild] = childrenArray;
20 if (firstChild.props.mdxType !== "code") return null;
21 return firstChild;
22}
23
24const Pre = ({ children }) => {
25 // Try to render a rich code snippet, fallback to plain
26 // <pre /> tag otherwise.
27 const codeChild = getCodeChild(children);
28 return codeChild ? transformCode(codeChild.props) : <pre>{children}</pre>;
29};
30
31const components = {
32 pre: Pre,
33};
34
35function MDXProvider({ children }) {
36 return <BaseMDXProvider components={components}>{children}</BaseMDXProvider>;
37}
38
39export default MDXProvider;

And that's it! We've now got rich and beautiful code snippets on the page. The cool thing with this approach is our <CodeSnippet /> component is very extensible. You can add line highlighting, file names, titles, and more. Anything you can imagine!

For more fun like theming, language support, and line highlights; refer to the prism-react-renderer README.

Bonus - Inline Code Snippets

A little bonus... have you noticed all the nicely formtted inline code snippets? Like this one: <MDXProvider />?

Well that's all done with prism-react-renderer and MDX as well!

Here's how.

Add an inlineCode option to the components in our <MDXProvider />

jsx
src/components/MDXProvider.js
1import React from "react";
2import { MDXProvider as BaseMDXProvider } from "@mdx-js/react";
3import InlineCode from "./InlineCode";
4
5// Simplified for brevity ...
6
7const Code = ({ children }) => {
8 if (!children) return null;
9
10 let lang = null;
11 let inlineCode = children;
12
13 // We'll be using double underscores to set our language for
14 // inline code snippets.
15 const RE = /__/;
16
17 if (RE.test(children)) {
18 const match = children.split("__");
19 lang = match[0];
20 inlineCode = match[1];
21 }
22
23 return <InlineCode lang={lang}>{inlineCode}</InlineCode>;
24};
25
26const components = {
27 inlineCode: Code,
28};
29
30function MDXProvider({ children }) {
31 return <BaseMDXProvider components={components}>{children}</BaseMDXProvider>;
32}
33
34export default MDXProvider;

Then create your <InlineCode /> component.

jsx
src/components/InlineCode.js
1import React from "react";
2import Highlight, { defaultProps } from "prism-react-renderer";
3
4function InlineCode({ lang, children }) {
5 const theme = useColorModeValue({ dark: darkTheme, light: lightTheme });
6
7 return (
8 <Highlight {...defaultProps} code={children} language={lang} theme={theme}>
9 {({ className, style, tokens, getLineProps, getTokenProps }) => (
10 <code className={`inline-code ${className}`} style={style}>
11 {tokens.map((line, i) => (
12 <span {...getLineProps({ line, key: i })}>
13 {line.map((token, key) => (
14 <span {...getTokenProps({ token, key })} />
15 ))}
16 </span>
17 ))}
18 </Code>
19 )}
20 </Highlight>
21 );
22}
23
24export default InlineCode;

And finally the styles to make it pretty 😍

css
src/components/InlineCode.css
1.inline-code {
2 font-size: 0.875rem;
3 line-height: 1.25rem;
4 white-space: nowrap;
5 border-radius: 0.25rem;
6 padding: 0.25rem 0.5rem;
7}

There you have it! Now you can write snippets inline with single backticks like this —

`css__font-size: 1rem;`

— or this —

`jsx__<MDXProvider />

which will render this font-size: 1rem; and this <MDXProvider />.

Hopefully you now have beautiful, rich, inline and multi-line code snippets. Drop a comment if this helped you out and send me a picture of your snippets on twitter @wray_tw!

About the Author

Hi 👋 I'm Tyler. I'm a software engineer with a passion for learning new things. I love solving hard problems and simplifying them down to their pieces. Presently residing in Utah with my two girls and beautiful wife.

Buy me a coffee