How to support admonition (or callout) in Markdown
Tags: webdev, react, markdown
Published: 2022-11-30 04:56:50

> This post was originally published on [my blog](https://rubiq.vercel.app/blog/remark-admonition)

Sometimes you may want to emphasize that command is destructive or note additional information in your blog or docs like this:

![Admonitions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6f628uivzi5gl0u6wb1m.png)

But this is not included in standard Markdown or GitHub Flavored Markdown. In this post, I will show you how to support this feature and notation with `remark` plugins.

> I assume you've already set up a static site generator that supports MDX by unified pipeline.

## Remark plugins

### Support notation

We'd like to add this custom notation for admonition:

```
:::info
Laboris dolore aliquip laboris irure.
:::
```

Fortunately, we don't have to write any code as there is [remark-directive](https://github.com/remarkjs/remark-directive) that does exactly what we need.

```bash
yarn add remark-directive
```

And add it to your own `unified` pipeline.

```ts
import remarkDirective from "remark-directive"
import remarkAdmonition from "./remarkAdmonitions" // We'll implement this next

const remarkPlugins = [remarkDirective, remarkAdmonition]
```

### Convert to `mdxJsxFlowElement`

But `remark-directive` doesn't do everything we need for displaying admonitions. To convert the block into an MDX component, define the following plugin:

```ts
import { visit } from "unist-util-visit"

export default function remarkAdmonition() {
  return (tree) => {
    visit(tree, (node) => {
      if (
        node.type === "textDirective" ||
        node.type === "leafDirective" ||
        node.type === "containerDirective"
      ) {
        if (!["info", "warn", "danger"].includes(node.name)) return
        // Store node.name before overwritten with "Alert".
        const status = node.name

        const data = node.data || (node.data = {})
        const tagName = node.type === "textDirective" ? "span" : "div"

        node.type = "mdxJsxFlowElement"
        node.name = "Admonition"
        node.attributes = [{ type: "mdxJsxAttribute", name: "status", value: status }]
      }
    })
  }
}
```

> Note that the node's name is `Admonition`. We'll have to add a custom MDX component that has the same name.

## React component and styles

Then we need a React component that accepts the `status` property. This value comes from any of `:::info`, `:::warn` or `:::danger`.

```tsx
import { HiInformationCircle, HiExclamationTriangle } from "react-icons/hi2"
import clsx from "clsx"
import React from "react"

import styles from "./styles.module.scss"

type SvgComponent = React.ComponentType<React.ComponentProps<"svg">>
type Status = "info" | "danger" | "warn"

type Props = {
  status?: Status
  title?: React.ReactNode
  children?: React.ReactNode
  className?: string
}

const statusIconMap: { [S in Status]: SvgComponent } = {
  info: HiInformationCircle,
  danger: HiExclamationTriangle,
  warn: HiExclamationTriangle,
}

export default function Message({ status = "info", children, className }: Props) {
  const Icon = statusIconMap[status]
  const statusClass = styles[`--${status}`]
  return (
    <aside className={clsx(styles.container, statusClass, className)}>
      <div>
        <Icon className={clsx(styles.icon, statusClass)} />
      </div>
      <div className={styles.text}>
        {children && <div className={styles.message}>{children}</div>}
      </div>
    </aside>
  )
}
```

```scss
$infoColor: #3182ce;
$warnColor: #e6a700;
$dangerColor: #e53e3e;

.container {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.5rem;
  padding: 1rem;
  border-radius: 0.5rem;
  border: 1px solid #e2e8f0;
  border-left-width: 4px;
  background: white;

  &.--info {
    border-left-color: $infoColor;
  }
  &.--warn {
    border-left-color: $warnColor;
  }
  &.--danger {
    border-left-color: $dangerColor;
  }
}

.text {
  display: grid;
  row-gap: 0.25rem;
}

.title {
  display: flex;
  align-items: center;
  column-gap: 0.5rem;
  font-weight: var(--camome-font-weight-bold);
  font-size: var(--camome-font-size-lg);

  &.--info {
    color: $infoColor;
  }
  &.--warn {
    color: $warnColor;
  }
  &.--danger {
    color: $dangerColor;
  }
}

.message {
  & > *:first-child {
    margin-top: 0;
  }

  & > *:last-child {
    margin-bottom: 0;
  }
}

.icon {
  width: 1.75rem;
  height: 1.75rem;

  &.--info {
    color: $infoColor;
  }
  &.--warn {
    color: $warnColor;
  }
  &.--danger {
    color: $dangerColor;
  }
}
```

## Pass it as a custom component

Now, register it as a custom component otherwise MDX complains that you've forgotten to import it!

```tsx
import { MDXProvider } from "@mdx-js/react"
import Admonition from "@/components/Admonition"

const components = {
  Admonition,
}

export default function Post(props) {
  return (
    <MDXProvider components={components}>
      <main {...props} />
    </MDXProvider>
  )
}
```

> Exact code could be different if you're using [mdx-bundler](https://github.com/kentcdodds/mdx-bundler), [Contentlayer](https://www.contentlayer.dev/), or anything else. But there should be a similar API for adding components.

## That's all

You can add another type like `:::note` or anything, and style the component as you want.

Thank you for reading!