MDX Content
Renders MDX content with component mapping. Install MDX Content from the VLLNT UI registry with the shadcn CLI.
Renders MDX content with component mapping. Part of the content family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add MDX Content to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/mdx-content.jsonSource
import { evaluate } from "@mdx-js/mdx";
import type React from "react";
import * as runtime from "react/jsx-runtime";
import ReactMarkdown, { type Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import { CodeBlock } from "../code-block/code-block";
import { StaticCode } from "../static-code/static-code";
type MDXContentProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
components?: Record<string, React.ComponentType<any>>;
content: string;
enableMDX?: boolean;
};
function extractCodeText(node: React.ReactNode): string {
return typeof node === "string"
? node.replace(/\n$/, "")
: String(node ?? "");
}
const MDXComponents: Components = {
a: ({ children, href, ...props }: React.ComponentProps<"a">) => (
<a
className="text-primary underline underline-offset-4 hover:text-primary/80 font-medium"
href={href}
{...props}
>
{children}
</a>
),
blockquote: ({ children, ...props }: React.ComponentProps<"blockquote">) => (
<blockquote
className="border-l border-primary pl-4 italic text-muted-foreground my-6 py-2 text-sm"
{...props}
>
{children}
</blockquote>
),
code: ({ children, className, ...props }: React.ComponentProps<"code">) => {
if (typeof className === "string" && className.startsWith("language-")) {
const language = className.replace(/^language-/, "");
const text = extractCodeText(children);
return <StaticCode code={text} language={language} />;
}
return (
<code
className="bg-muted px-1 py-0.5 rounded text-sm font-mono"
{...props}
>
{children}
</code>
);
},
em: ({ children, ...props }: React.ComponentProps<"em">) => (
<em className="italic" {...props}>
{children}
</em>
),
h1: ({ children, ...props }: React.ComponentProps<"h1">) => (
<h1 className="text-2xl font-semibold mt-8 mb-4" {...props}>
{children}
</h1>
),
h2: ({ children, ...props }: React.ComponentProps<"h2">) => (
<h2 className="text-xl font-semibold mt-6 mb-3" {...props}>
{children}
</h2>
),
h3: ({ children, ...props }: React.ComponentProps<"h3">) => (
<h3 className="text-lg font-semibold mt-4 mb-2" {...props}>
{children}
</h3>
),
hr: ({ ...props }: React.ComponentProps<"hr">) => (
<hr className="my-8 border-border" {...props} />
),
li: ({ children, ...props }: React.ComponentProps<"li">) => (
<li
className="mb-2 leading-relaxed text-muted-foreground text-sm pl-2"
{...props}
>
{children}
</li>
),
ol: ({ children, ...props }: React.ComponentProps<"ol">) => (
<ol
className="list-decimal list-outside mb-6 space-y-2 ml-6 text-muted-foreground text-sm"
{...props}
>
{children}
</ol>
),
p: ({ children, ...props }: React.ComponentProps<"p">) => (
<p
className="mb-6 leading-relaxed text-muted-foreground text-sm max-w-none"
{...props}
>
{children}
</p>
),
pre: ({ children }: React.ComponentProps<"pre">) => (
<div className="contents">{children}</div>
),
strong: ({ children, ...props }: React.ComponentProps<"strong">) => (
<strong className="font-semibold" {...props}>
{children}
</strong>
),
ul: ({ children, ...props }: React.ComponentProps<"ul">) => (
<ul
className="list-disc list-outside mb-6 space-y-2 ml-6 text-muted-foreground text-sm"
{...props}
>
{children}
</ul>
),
};
const proseClasses = [
"prose prose-lg dark:prose-invert max-w-none",
"prose-headings:font-bold prose-headings:tracking-tight",
"prose-h1:text-2xl prose-h1:mt-8 prose-h1:mb-4",
"prose-h2:text-xl prose-h2:mt-6 prose-h2:mb-3",
"prose-h3:text-lg prose-h3:mt-4 prose-h3:mb-2",
"prose-p:leading-relaxed prose-p:mb-6 prose-p:text-muted-foreground prose-p:text-sm prose-p:max-w-none",
"prose-ul:my-6 prose-ul:ml-6 prose-ul:list-disc prose-ul:list-outside",
"prose-ol:my-6 prose-ol:ml-6 prose-ol:list-decimal prose-ol:list-outside",
"prose-li:mb-2 prose-li:leading-relaxed prose-li:pl-2",
"prose-strong:font-semibold prose-em:italic",
"prose-a:text-primary prose-a:underline prose-a:underline-offset-4 hover:prose-a:text-primary/80",
"prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono",
"prose-pre:my-6 prose-pre:overflow-x-auto prose-pre:rounded-lg prose-pre:border prose-pre:bg-zinc-950 prose-pre:py-4 prose-pre:font-mono prose-pre:text-sm prose-pre:text-white prose-pre:shadow-lg dark:prose-pre:bg-zinc-900",
"prose-blockquote:border-l prose-blockquote:border-primary prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-muted-foreground prose-blockquote:my-6 prose-blockquote:py-2",
"prose-hr:my-8 prose-hr:border-border",
"prose-table:w-full prose-table:border-collapse prose-table:border prose-table:border-border",
"prose-th:border prose-th:border-border prose-th:bg-muted prose-th:p-2 prose-th:text-left prose-th:font-medium",
"prose-td:border prose-td:border-border prose-td:p-2",
"prose-img:rounded-lg prose-img:border prose-img:border-border prose-img:shadow-lg",
].join(" ");
const markdownPlugins = [remarkGfm];
function removeImportStatements(
content: string,
componentNames: string[],
): string {
let processed = content.replaceAll(/^import\s+.*CodeBlock.*from.*$/gm, "");
componentNames.forEach((name) => {
processed = processed.replaceAll(
new RegExp(`^import\\s+.*${name}.*from.*$`, "gm"),
"",
);
});
return processed;
}
function buildCustomComponents(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
injectedComponents: Record<string, React.ComponentType<any>>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Record<string, React.ComponentType<any>> {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
CodeBlock: CodeBlock as React.ComponentType<any>,
...injectedComponents,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const EMPTY_MDX_COMPONENTS: Record<string, React.ComponentType<any>> = {};
export async function MDXContent({
components = EMPTY_MDX_COMPONENTS,
content,
enableMDX = true,
}: MDXContentProps) {
const componentNames = Object.keys(components);
const processedContent = removeImportStatements(content, componentNames);
const contentWithoutCodeBlocks = processedContent.replaceAll(
/```[\S\s]*?```/g,
"",
);
const hasJSX = /<[A-Z][A-Za-z]*/.test(contentWithoutCodeBlocks);
const customComponents = buildCustomComponents(components);
const allComponents = { ...MDXComponents, ...customComponents };
if (enableMDX && hasJSX) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let Component: React.ComponentType<{ components: any }> | undefined;
try {
const result = await evaluate(processedContent, {
...runtime,
baseUrl: import.meta.url,
});
Component = result.default;
} catch (error) {
console.error("Error rendering MDX:", error);
}
if (Component) {
return (
<div className={proseClasses}>
<Component components={allComponents} />
</div>
);
}
return (
<div className={proseClasses}>
<ReactMarkdown
components={allComponents}
remarkPlugins={markdownPlugins}
>
{content}
</ReactMarkdown>
</div>
);
}
return (
<div className={proseClasses}>
<ReactMarkdown components={allComponents} remarkPlugins={markdownPlugins}>
{content}
</ReactMarkdown>
</div>
);
}
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.