Working With MDX Custom Elements and Shortcodes
MDX is a killer feature for things like blogs, slide decks and component documentation. It allows you to write Markdown without worrying about HTML elements, their formatting and placement while sprinkling in the magic of custom React components when necessary.
Let’s harness that magic and look at how we can customize MDX by replacing Markdown elements with our own MDX components. In the process, we’ll introduce the concept of “shortcodes” when using those components.
As a heads up, the code snippets here are based on GatsbyJS and React, but MDX can be written with different frameworks as well. If you need a primer on MDX, start here first. This article extends that one with more advanced concepts.
Setting up a layout
We almost always want to render our MDX-based pages in a common layout. That way, they can be arranged with other components on our website. We can specify a default Layout
component with the MDX plugin we’re using. For example. we can define a a layout with the gatsby-plugin-mdx
plugin like this:
{
resolve: `gatsby-plugin-mdx`,
options: {
defaultLayouts: {
default: path.resolve('./src/templates/blog-post.js'),
},
// ...other options
}
}
This would require the src/templates/blog-post.js
file to contain a component that would render the children
prop it receives.
import { MDXRenderer } from 'gatsby-plugin-mdx';
function BlogPost({ children }) {
return (
<div>{children}</div>
);
}
export default BlogPost;
If we are programmatically creating pages, we’d have to use a component named MDXRenderer
to achieve the same thing, as specified in the Gatsby docs.
Custom Markdown elements
While MDX is a format where that lets us write custom HTML and React components, its power is rendering Markdown with custom content. But what if we wanted to customize how these Markdown elements render on screen?
We could surely write a remark plugin for it, but MDX provides us with a better, simpler solution. By default, these are some of the elements being rendered by Markdown:
Name | HTML Element | MDX Syntax |
---|---|---|
Paragraph |
|
|
Heading 1 |
|
# |
Heading 2 |
|
## |
Heading 3 |
|
### |
Heading 4 |
|
#### |
Heading 5 |
|
##### |
Heading 6 |
|
###### |
Unordered List |
|
- |
Ordered List |
|
1. |
Image |
|
![alt](https://image-url) |
To replace these defaults with our custom React components, MDX ships with a Provider
component named MDXProvider
. It relies on the React Context API to inject new custom components and merge them into the defaults provided by MDX.
import React from 'react';
import { MDXProvider } from "@mdx-js/react";
import Image from './image-component';
function Layout({ children }) {
return (
<MDXProvider
components={{
h1: (props) => <h1 {...props} className="text-xl font-light" />
img: Image,
}}
>
{children}
</MDXProvider>
);
}
export default Layout;
In this example, any H1 heading (#
) in the MDX file will be replaced by the custom implementation specified in the Provider
component’s prop while all the other elements will continue to use the defaults. In other words, MDXProvider
is able to take our custom markup for a H1 element, merge it with MDX defaults, then apply the custom markup when we write Heading 1 (#
) in an MDX file.
MDX and custom components
Customizing MDX elements is great, but what if we want to introduce our own components into the mix?
---
title: Importing Components
---
import Playground from './Playground';
Here is a look at the `Playground` component that I have been building:
<Playground />
We can import a component into an MDX file and use it the same way we would any React component. And, sure, while this works well for something like a component demo in a blog post, what if we want to use Playground on all blog posts? It would be a pain to import them to all the pages. Instead. MDX presents us with the option to use shortcodes. Here’s how the MDX documentation describes shortcodes:
[A shortcode] allows you to expose components to all of your documents in your app or website. This is a useful feature for common components like YouTube embeds, Twitter cards, or anything else frequently used in your documents.
To include shortcodes in an MDX application, we have to rely on the MDXProvider
component again.
import React from 'react';
import { MDXProvider } from "@mdx-js/react";
import Playground from './playground-wrapper';
function Layout({ children }) {
return (
<MDXProvider
components={{
h1: (props) => <h1 {...props} className="text-xl font-light" />
Playground,
}}
>
{children}
</MDXProvider>
);
}
export default Layout;
Once we have included custom components into the components object, we can proceed to use them without importing in MDX files.
---
title: Demoing concepts
---
Here's the demo for the new concept:
<Playground />
> Look ma! No imports
Directly manipulating child components
In React, we get top-level APIs to manipulate children with React.Children
. We can use these to pass new props to child components that change their order or determine their visibility. MDX provides us a special wrapper component to access the child components passed in by MDX.
To add a wrapper, we can use the MDXProvider
as we did before:
import React from "react";
import { MDXProvider } from "@mdx-js/react";
const components = {
wrapper: ({ children, ...props }) => {
const reversedChildren = React.Children.toArray(children).reverse();
return <>{reversedChildren}</>;
},
};
export default (props) => (
<MDXProvider components={components}>
<main {...props} />
</MDXProvider>
);
This example reverses the children so that they appear in reverse order that we wrote it in.
We can even go wild and animate all of MDX children as they come in:
import React from "react";
import { MDXProvider } from "@mdx-js/react";
import { useTrail, animated, config } from "react-spring";
const components = {
wrapper: ({ children, ...props }) => {
const childrenArray = React.Children.toArray(children);
const trail = useTrail(childrenArray.length, {
xy: [0, 0],
opacity: 1,
from: { xy: [30, 50], opacity: 0 },
config: config.gentle,
delay: 200,
});
return (
<section>
{trail.map(({ y, opacity }, index) => (
<animated.div
key={index}
style={{
opacity,
transform: xy.interpolate((x, y) => `translate3d(${x}px,${y}px,0)`),
}}
>
{childrenArray[index]}
</animated.div>
))}
</section>
);
},
};
export default (props) => (
<MDXProvider components={components}>
<main {...props} />
</MDXProvider>
);
Wrapping up
MDX is designed with flexibility out of the box, but extending with a plugin can make it do even more. Here’s what we were just able to do in a short amount of time, thanks to gatsby-plugin-mdx
:
- Create default Layout components that help format the MDX output.
- Replace default HTML elements rendered from Markdown with custom components
- Use shortcodes to get rid of us of importing components in every file.
- Manipulate children directly to change the MDX output.
Again, this is just another drop in the bucket as far as what MDX does to help make writing content for static sites easier.
More on MDX
The post Working With MDX Custom Elements and Shortcodes appeared first on CSS-Tricks.