MDX custom components using data-attributes

529 Views Asked by At

Is it possible to define custom MDX components using a data-attribute of the HTML element?

I'm working on a Next.js blog using contentlayer for MDX and Rehype Pretty Code for syntax highlighting code blocks. Below is the HTML output structure from Rehype Pretty code. I would like to create a custom MDX component for the below HTML, but the root elemen is a <div>. So is it possible to target a div element using its data attribute - in this case div[data-rehype-pretty-code-fragment] to create a custom MDX component?

<div data-rehype-pretty-code-fragment>
    <div data-rehype-pretty-code-title="" data-language="js">sample.js</div>
    <pre data-language="js">
        <code data-line-numbers data-line-numbers-max-digits="2">
            </span data-line="">
            </span style="color:#C678DD">const</span>
            <!-- more span elements ... -->
        </code>
    </pre>
    <div data-rehype-pretty-code-caption>this is a sample caption</div>
</div>

above code block is shortened for brevity

Custom MDX Components:

is it possible to do this?

export const components: MDXComponents = {
    // Add a custom component.
    MyComponent: () => <div>Hello World!</div>,
    div[data-rehype-pretty-code-fragment]: (props) => <CodeBlock {...props}  />, // is this possible?
};

My Requirement

What I'm trying to do is to add additional DOM elements & classes to the code-block output like below classes codeBlockRoot, customContent & moreCustomContent using a Custom JSX component and add styles to the entire code-block. Right now I'm having to use div[data-rehype-pretty-code-fragment] to add styles to the code block:

<div class="codeBlockRoot" data-rehype-pretty-code-fragment>
    <div class="customContent">this is custom HTML</div>
    <div data-rehype-pretty-code-title="" data-language="js">
        sample.js
        <div class="moreCustomContent">more custom content</div>
    </div>
    <pre data-language="js">
        <code data-line-numbers data-line-numbers-max-digits="2">
            </span data-line="">
            </span style="color:#C678DD">const</span>
            <!-- more span elements ... -->
        </code>
    </pre>
    <div data-rehype-pretty-code-caption>this is a sample caption</div>
</div>
1

There are 1 best solutions below

0
Igor Danchenko On

I believe the following solution meets your criteria.

// mdx-components.tsx

import * as React from "react";
import type { MDXComponents } from "mdx/types";
import CodeBlock from "@/components/CodeBlock";

declare module "react" {
  interface HTMLAttributes<T> {
    [key: `data-${string}`]: unknown;
  }
}

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    div: ({ children, ...rest }) =>
      React.createElement(
        rest["data-rehype-pretty-code-fragment"] !== undefined
          ? CodeBlock
          : "div",
        rest,
        children
      ),
    ...components,
  };
}
// CodeBlock.tsx

import * as React from "react";

export default function CodeBlock({
  className,
  children,
  ...rest
}: React.JSX.IntrinsicElements["div"]) {
  return (
    <div
      className={[className, "codeBlockRoot"].filter(Boolean).join(" ")}
      {...rest}
    >
      {React.Children.map(children, (child) =>
        React.isValidElement(child) &&
        child.props["data-rehype-pretty-code-title"] !== undefined ? (
          <>
            <div className="customContent">this is custom HTML</div>
            {React.cloneElement(child, child.props, [
              ...React.Children.toArray(child.props.children),
              <div key="moreCustomContent" className="moreCustomContent">
                more custom content
              </div>,
            ])}
          </>
        ) : (
          child
        )
      )}
    </div>
  );
}