How to Create Custom MDX Components for Responsive Images and Links in Next.js
Use a single MDX component registry (mdx-components.tsx) to override default HTML elements, mapping <img> to Next.js <Image> for responsive optimization and <a> to a smart link handler that distinguishes external URLs, internal blog references, and client-side navigation.
The woosal1337/blog repository demonstrates a clean, centralized approach to enhancing MDX content in Next.js applications. By registering custom components for standard HTML tags, you gain full control over image performance, link behavior, and visual styling—without cluttering your MDX files with framework-specific syntax.
Setting Up the MDX Components Registry
Next.js App Router projects use mdx-components.tsx at the project root to define component mappings. This file exports a useMDXComponents function that receives and extends the default MDX component set.
Creating this registry is the first step toward customizing how MDX renders standard elements:
import type { MDXComponents } from "mdx/types";
import Image, { ImageProps } from "next/image";
import Link from "next/link";
import { cn } from "@/lib/utils";
import { LinkLeadingIcon } from "@/components/ds/icon-link";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
// Custom overrides go here
img: /* responsive image component */,
a: /* smart link component */,
...components,
};
}
This pattern keeps all MDX customization in one maintainable location.
Creating a Responsive Image Component
The <img> override in mdx-components.tsx leverages Next.js's optimized Image component to deliver responsive, lazy-loaded images with automatic format conversion.
Key Implementation Details
sizesprop: Instructs the browser which image width to request at different viewport breakpoints—100vwon mobile,692pxon larger screens- Intrinsic dimensions: Fixed
width={1200}andheight={675}maintain layout stability during loading - Fluid scaling:
w-full h-autoensures the image adapts to its container while preserving aspect ratio
img: (props) => (
// eslint-disable-next-line jsx-a11y/alt-text
<Image
sizes="(max-width: 768px) 100vw, 692px"
width={1200}
height={675}
className="my-8 h-auto w-full rounded-[12px] border border-line"
{...(props as ImageProps)}
/>
),
The src and alt attributes pass through from the MDX source, so authors write standard markdown syntax while receiving Next.js optimization benefits automatically.
MDX Usage
Authors continue using familiar syntax without framework awareness:
# My Post
Here is a responsive image:
<img src="/images/hero.jpg" alt="Hero shot" />
The resulting output includes WebP generation, blur-up placeholders, and responsive srcset generation—all transparent to content creators.
Building a Smart Link Component with Three Behaviors
The <a> override in mdx-components.tsx implements intelligent routing logic that distinguishes three URL types and applies appropriate rendering strategies.
Behavior 1: External URLs
HTTP/HTTPS links render as standard anchor tags with security attributes and visual indicators:
target="_blank"andrel="noopener noreferrer"for security- Leading icon via
LinkLeadingIconcomponent for external link affordance - Consistent underline styling with hover transitions
Behavior 2: Internal Blog References
Paths starting with /blog/ receive the same icon treatment as external links, maintaining visual consistency when referencing other posts—readers recognize these as content navigation.
Behavior 3: Standard Internal Routes
All other internal paths use Next.js's <Link> component for client-side navigation without full page reloads.
Complete Implementation
a: ({ children, href, ...props }) => {
if (!href) return <span {...props}>{children}</span>;
const isBlogReference = href.startsWith("/blog/");
if (href.startsWith("http")) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={cn(
"group/link whitespace-nowrap text-ink underline decoration-line underline-offset-[3px] transition-colors duration-200 ease-house hover:decoration-ink-soft"
)}
{...props}
>
<LinkLeadingIcon href={href} />
{children}
</a>
);
}
return (
<Link
href={href}
className={cn(
"text-ink underline decoration-line underline-offset-[3px] transition-colors duration-200 ease-house hover:decoration-ink-soft",
isBlogReference && "whitespace-nowrap"
)}
>
{isBlogReference ? <LinkLeadingIcon href={href} /> : null}
{children}
</Link>
);
},
MDX Usage
Links work naturally with automatic behavior detection:
Check out the <a href="https://example.com">external site</a> or read more
about this topic in the <a href="/blog/another-post">next blog post</a>.
Supporting Utilities
Class Name Concatenation (lib/utils.tsx)
Both components rely on the cn helper for clean Tailwind class merging:
export function cn(...inputs: (string | undefined | false | null)[]) {
return inputs.filter(Boolean).join(" ");
}
This utility eliminates duplicate classes and handles conditional styling gracefully. Located in lib/utils.tsx, it's shared across the entire application.
Link Icon Component (components/ds/icon-link.tsx)
The LinkLeadingIcon component renders visual indicators for external and blog-reference links, imported from components/ds/icon-link.tsx. This separation keeps the MDX component registry focused on logic while delegating presentation details.
Key Files and Architecture
| File | Purpose |
|---|---|
mdx-components.tsx |
Central registry overriding default MDX elements |
lib/utils.tsx |
Shared helpers including cn for class merging |
components/ds/icon-link.tsx |
Visual indicators for special link types |
components/ds/tag.tsx |
Badge component for MDX tags |
next/link |
Client-side navigation for internal routes |
This architecture ensures that design changes propagate consistently—modifying styles in the registry updates every MDX file site-wide.
Summary
- Register custom MDX components in
mdx-components.tsxto override default HTML element rendering - Map
<img>tonext/imagefor automatic responsive optimization, lazy loading, and format conversion - Implement URL-type detection in your
<a>override to handle external, blog-reference, and standard internal links appropriately - Use shared utilities like
cnfromlib/utils.tsxto maintain consistent Tailwind styling across all components - Preserve clean MDX syntax so authors write standard HTML tags while receiving framework-optimized output
Frequently Asked Questions
How do I pass additional props to custom MDX image components?
Spread the props parameter onto your component with type assertion. The implementation uses {...(props as ImageProps)} to pass through src, alt, and other standard attributes while maintaining TypeScript safety. Additional custom props can be destructured separately before spreading.
Can I customize the responsive breakpoints for images?
Modify the sizes prop value in your img component mapping. The current implementation uses (max-width: 768px) 100vw, 692px—change the breakpoint or fallback width to match your layout's content area. The width and height props set intrinsic dimensions, not display size.
Why distinguish /blog/ references from other internal links?
Blog references receive the same icon treatment as external links to signal "related reading" to users, while standard internal links navigate silently. This visual consistency helps readers recognize content relationships without requiring authors to remember special syntax.
Do I need to install additional packages for this setup?
No—this approach uses built-in Next.js features. next/image and next/link ship with the framework. The mdx-components.tsx convention is standard for Next.js App Router MDX integration. Only standard dependencies like tailwindcss for styling are assumed.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →