# How to Create Custom MDX Components for Responsive Images and Links in Next.js

> Learn to create custom MDX components in Next.js. Optimize images with Next/Image and handle links intelligently for responsive content and seamless navigation.

- Repository: [Ege Chelebi/blog](https://github.com/woosal1337/blog)
- Tags: how-to-guide
- Published: 2026-08-06

---

**Use a single MDX component registry ([`mdx-components.tsx`](https://github.com/woosal1337/blog/blob/main/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`](https://github.com/woosal1337/blog/blob/main/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:

```tsx
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`](https://github.com/woosal1337/blog/blob/main/mdx-components.tsx) leverages Next.js's optimized `Image` component to deliver responsive, lazy-loaded images with automatic format conversion.

### Key Implementation Details

- **`sizes` prop**: Instructs the browser which image width to request at different viewport breakpoints—`100vw` on mobile, `692px` on larger screens
- **Intrinsic dimensions**: Fixed `width={1200}` and `height={675}` maintain layout stability during loading
- **Fluid scaling**: `w-full h-auto` ensures the image adapts to its container while preserving aspect ratio

```tsx
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:

```mdx

# 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`](https://github.com/woosal1337/blog/blob/main/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"` and `rel="noopener noreferrer"` for security
- Leading icon via `LinkLeadingIcon` component 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

```tsx
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:

```mdx
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`](https://github.com/woosal1337/blog/blob/main/lib/utils.tsx))

Both components rely on the `cn` helper for clean Tailwind class merging:

```tsx
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`](https://github.com/woosal1337/blog/blob/main/lib/utils.tsx), it's shared across the entire application.

### Link Icon Component ([`components/ds/icon-link.tsx`](https://github.com/woosal1337/blog/blob/main/components/ds/icon-link.tsx))

The `LinkLeadingIcon` component renders visual indicators for external and blog-reference links, imported from [`components/ds/icon-link.tsx`](https://github.com/woosal1337/blog/blob/main/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`](https://github.com/woosal1337/blog/blob/main/mdx-components.tsx) | Central registry overriding default MDX elements |
| [`lib/utils.tsx`](https://github.com/woosal1337/blog/blob/main/lib/utils.tsx) | Shared helpers including `cn` for class merging |
| [`components/ds/icon-link.tsx`](https://github.com/woosal1337/blog/blob/main/components/ds/icon-link.tsx) | Visual indicators for special link types |
| [`components/ds/tag.tsx`](https://github.com/woosal1337/blog/blob/main/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.tsx`](https://github.com/woosal1337/blog/blob/main/mdx-components.tsx) to override default HTML element rendering
- **Map `<img>` to `next/image`** for 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 `cn` from [`lib/utils.tsx`](https://github.com/woosal1337/blog/blob/main/lib/utils.tsx) to 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`](https://github.com/woosal1337/blog/blob/main/mdx-components.tsx) convention is standard for Next.js App Router MDX integration. Only standard dependencies like `tailwindcss` for styling are assumed.