Setting Up MDX With Tanstack Start: All You Need To Know

Written By: Enoch Klu

Publish Date: Saturday, 3 January 2026

#mdx
#tanstack
#react
#web


My Motivation

I began playing around with Tanstack Start in late 2025 for a website I was working on. For the most part, everything went on smoothly until I had to setup the docs pages! If you have worked on documentation, you know just how content-heavy it can be, and it wasn’t that I didn’t know how to build them: I just didn’t have enough time. Now, I needed a way to create these pages as quickly as I could, which is exactly when MDX came to mind.


Since I was pretty new to Tanstack Start, I didn’t quite know how to set it up. So naturally, I visited the oracle that goes by the name of Google. After a while of serious digging, I didn’t have much to show for it, and it wasn’t long before I realised what was wrong with my approach.


I took a step back to rethink the process and then I asked myself “what if I set up MDX with Vite instead?” Since my Tanstack Start project was running on Vite, this path felt promising. So I gave it a shot, it worked, and that is why I wrote this article for you.


What The Heck Is MDX?

MDX or Markdown eXtended as it is rarely known, allows you to write JSX inside Markdown. Imagine having the flexibility of Markdown with the power of your predefined components; that’s the idea.


If you’re not familiar with MDX, this example should show you how it looks in practice:


blog-post-1.mdx
// Define the frontmatter (used for content metadata like the post title for instance)---title: 'Blog Post 1'published_at: '2026-01-01'---// Import your componentimport { Banner } from '@/components/banner'// Use Markdown-style elements# Welcome To Blog Post 1!// Use your component (it works!)<Banner src="http://test.com/banner.png" />


Now that the introduction is out of the way, let’s install MDX.


Installing & Configuring MDX

Since we are setting up MDX with Tanstack Start running on Vite, here’s how we’ll install MDX:


script.sh
npm install @mdx-js/react @mdx-js/rollup

After installation, we’ll configure vite.config.ts like so:


vite.config.ts
/* Import Vite config dependencies */import { defineConfig } from 'vite'import { devtools } from '@tanstack/devtools-vite'import { tanstackStart } from '@tanstack/react-start/plugin/vite'import viteReact from '@vitejs/plugin-react'import viteTsConfigPaths from 'vite-tsconfig-paths'import tailwindcss from '@tailwindcss/vite'import { nitro } from 'nitro/vite'import mdx from '@mdx-js/rollup' // [!code highlight]/* Define the config */const config = defineConfig({  plugins: [    devtools(),    nitro(),    viteTsConfigPaths({      projects: ['./tsconfig.json'],    }),    tailwindcss(),    tanstackStart(),    { // [!code highlight]      enforce: 'pre', // [!code highlight]      ...mdx({ // [!code highlight]        providerImportSource: '@mdx-js/react', // [!code highlight]        /* Other MDX config options... */ // [!code highlight]      }), // [!code highlight]    }, // [!code highlight]    viteReact(),  ],})export default config

Set Up Table Rendering

The original setup does not support tables and will not render those defined purely in Markdown. In order to have Markdown-style tables converted into React components, we’ll need to install a plugin. For this guide we will install remark-gfm which enables support for tables, footnotes, and other Markdown elements.


script.sh
npm install remark-gfm

Finally, let’s update our vite.config.ts file with the remark-gfm plugin:


vite.config.ts
/* Import Vite config dependencies */import { defineConfig } from 'vite'import { devtools } from '@tanstack/devtools-vite'import { tanstackStart } from '@tanstack/react-start/plugin/vite'import viteReact from '@vitejs/plugin-react'import viteTsConfigPaths from 'vite-tsconfig-paths'import tailwindcss from '@tailwindcss/vite'import { nitro } from 'nitro/vite'import mdx from '@mdx-js/rollup'/* Import `remark-gfm` */import remarkGfm from 'remark-gfm' // [!code highlight]/* Define the config */const config = defineConfig({  plugins: [    devtools(),    nitro(),    viteTsConfigPaths({      projects: ['./tsconfig.json'],    }),    tailwindcss(),    tanstackStart(),    {      enforce: 'pre',      ...mdx({        providerImportSource: '@mdx-js/react',        remarkPlugins: [[remarkGfm, { strict: true }]], // [!code highlight]      }),    },    viteReact(),  ],})export default config

That’s it! Now you should have tables rendering like they should.



Since we have MDX installed and configured, let’s add some custom components.


Using Custom Components

We can render custom components in 2 ways:

Option 1: Wrapping MDXProvider Around Your MDX Content

I personally prefer and recommend this approach where possible. Since it is a context provider, it allows you to pass your custom components once to be used especially in layout routes/components. Here’s how you use it:


src/components/mdx.tsx
import type { MDXComponents } from 'mdx/types'import { cn } from '@/lib/utils'export const CustomMdxComponents: MDXComponents = {  h1: (props) => <h1 className="text-4xl font-bold py-8" {...props} />,  h2: (props) => <h2 className="text-3xl font-bold py-8" {...props} />,  h3: (props) => <h3 className="text-2xl font-bold py-8" {...props} />,  h4: (props) => <h4 className="text-xl font-bold py-8" {...props} />,  h5: (props) => <h4 className="text-lg font-bold py-8" {...props} />,  h6: (props) => <h4 className="text-md font-bold py-8" {...props} />,  p: (props) => <p className="text-md font-medium" {...props} />,  table: (props) => (    <div className="table">      <table className="border-collapse w-full" {...props} />    </div>  ),  th: (props) => <th className="border-gray-200 border p-2" {...props} />,  td: (props) => <td className="border-gray-200 border p-2" {...props} />,  code: ({ className, ...props }) => (    <code className={cn('text-sm font-medium rounded-sm border w-fit inline px-1 py-0.5', className)} {...props} />  ),  /* Define other custom components... they can be imported React components too! */  Box: (props) => <div className="flex" {...props} />}



Option 2: Importing The MDX File To Pass Your Custom Components Directly

This approach allows you to pass your custom components directly as props to each MDX file. That’s because MDX files are components, and this method enables you to pass unique components to each MDX file wherever you see fit. Here’s how you use it:


src/routes/blog/post-1.tsx
import { createFileRoute } from '@tanstack/react-router'/* Import the `MDXComponents` type for type safety */import type { MDXComponents } from 'mdx/types' // [!code highlight]/* Import your MDX file */import Post1 from '@/content/blog/post-1.mdx'  // [!code highlight]export const Route = createFileRoute('/blog')({  component: BlogLayoutRoute,  context: () => ({ title: 'Blog' }),})/* Define custom components */const Post1Components: MDXComponents = { // [!code highlight]  h1: (props) => <h1 className="text-4xl font-bold py-8" {...props} />, // [!code highlight]  h2: (props) => <h2 className="text-3xl font-bold py-8" {...props} />, // [!code highlight]} // [!code highlight]function Post1Route() {  return (    <div className="w-full p-8 px-48">      <Post1Content components={Post1Components} /> {/* [!code highlight] */}    </div>  )}

Notice how we’re passing different components for Post 1 and Post 2? If having different components for different pages benefits you, then this is definitely the way to go.



Final Thoughts

MDX offers a lot of benefits when crafting pages that are easy to edit and manage. That said, I hope you successfully set up your app, and I can’t wait to see what you build with this.

© 2026 Enoch Klu. All Rights Reserved