Code Block MDX

Create a MDX Code Block component with syntax highlighting and copy button.

ReactMDXComponents

Preview

.tsx
import type { ComponentProps } from "react";

const MyComponent = (props: ComponentProps<"div">) => {
  return <div {...props}>Hello, World!</div>;
};

export default MyComponent;

Code Block MDX + Copy Button + Shiki Highlighter

Installation

shadcn/ui

shadcn/ui Command
pnpm dlx shadcn@latest add https://code-blocks.pheralb.dev/r/mdx-shiki.json

Manual

Before creating the MDX component, make sure you have the basic component structure:

reactCode Block

The main structure of the Code Block component with header and content areas.

  1. Install the following dependencies:
MDX Types
pnpm i @types/mdx -D
  1. Create the Copy Button component:
reactCopy Button

A button component to copy content to the clipboard.

  1. Create a react-to-text utility to get plain text from pre elements:
.ts
import {
  isValidElement,
  type ReactNode,
  type JSXElementConstructor,
} from "react";

type ResolverMap = Map<
  string | JSXElementConstructor<object>,
  (props: object) => string
>;

const reactToText = (node: ReactNode, resolvers?: ResolverMap): string => {
  if (node == null || typeof node === "boolean") return "";

  if (typeof node === "string" || typeof node === "number") return String(node);

  if (Array.isArray(node))
    return node.map((n) => reactToText(n, resolvers)).join("");

  if (isValidElement(node)) {
    const resolver = resolvers?.get(
      node.type as string | JSXElementConstructor<object>,
    );
    if (resolver) return resolver(node.props as object);
    return reactToText(
      (node.props as { children?: ReactNode }).children,
      resolvers,
    );
  }

  return "";
};

export { reactToText };

Highlighter Setup

Setup your MDX highlighter:

  1. Setup data-title & data-language properties:
shikiAdd Properties

How to add custom properties to pre elements using a custom Shiki transformer.

  1. Setup Shiki highlighter:
shikiShiki Highlighter

The main highlighter utility to render syntax highlighted code.

  1. Setup Shiki Markdown integration:
shikiShiki Highlighter#markdownmdx-integration

The main highlighter utility to render syntax highlighted code.

  1. Create your PreShikiComponent component:
.tsx
import type { ComponentProps } from "react";
import type { MDXComponents } from "mdx/types";

import { reactToText } from "@/utils/react-to-text";

import {
  CodeBlock,
  CodeBlockHeader,
  CodeBlockContent,
  CodeBlockIcon,
  CodeBlockGroup,
} from "@/components/code-block/code-block";
import { CopyButton } from "@/components/code-block/copy-button";

interface PreProps extends ComponentProps<"pre"> {
  ["data-language"]: string;
  ["data-title"]?: string;
}

const PreShikiComponent: MDXComponents = {
  pre: ({ children, ...props }: PreProps) => {
    const content = reactToText(children);
    const title = props["data-title"];
    const language = props["data-language"];
    return (
      <CodeBlock>
        <CodeBlockHeader>
          <CodeBlockGroup>
            <CodeBlockIcon language={language} />
            <span>{title ?? `.${language}`}</span>
          </CodeBlockGroup>
          <CopyButton content={content} />
        </CodeBlockHeader>
        <CodeBlockContent>
          <pre {...props}>{children}</pre>
        </CodeBlockContent>
      </CodeBlock>
    );
  },
};

export { PreShikiComponent };
  1. Usage:

In your MDX Components object, add the new PreShikiComponent component:

.ts
import type { MDXComponents as MDXComponentsType } from "mdx/types";

import { PreShikiComponent } from "@/components/code-block/pre-shiki";

const MDXComponents: MDXComponentsType = {
  ...PreShikiComponent,
};