Migrating Away from VitePress Default Theme

VitePress ships with a polished default theme that handles navigation, sidebars, dark mode, and markdown styling out of the box. It's a great starting point, but there are compelling reasons to eventually replace it with your own implementation.

Why Migrate?

Design ownership. The default theme has opinions about spacing, typography, and layout. Overriding these through CSS variables only gets you so far. Eventually you want components that match your design system from the ground up.

Bundle size. The default theme includes components you may never use—search, sponsor links, team pages, carbon ads integration. Even tree-shaking can't eliminate everything when you're extending the theme. My initial theme bundle was 613KB. After the migration, the entry point dropped to 1.3KB with page-specific code lazy-loaded on demand.

The Incremental Approach

Ripping out the default theme all at once is a recipe for a broken site. Instead, I replaced it piece by piece, testing at each step.

Phase 1: Extend and Override

Start by extending the default theme rather than replacing it:

typescript
// .vitepress/theme/index.ts
import DefaultTheme from "vitepress/theme-without-fonts";
import Layout from "./layouts/Layout.vue";

export default {
  extends: DefaultTheme,
  Layout,
};

VitePress's layout system exposes slots for content projection. You can inject custom components into specific areas without touching the rest:

vue
<template>
  <DefaultTheme.Layout>
    <template #nav-bar-content-after>
      <MyCustomNavItem />
    </template>
    <template #sidebar-nav-before>
      <MySidebarHeader />
    </template>
  </DefaultTheme.Layout>
</template>

This lets you introduce custom components gradually while the default theme handles everything else.

Phase 2: Remap Component Imports

VitePress lets you alias component imports through Vite's configuration. This is powerful for replacing default components wholesale:

typescript
// .vitepress/config.mts
export default defineConfig({
  vite: {
    resolve: {
      alias: {
        "./VPSidebar.vue": fileURLToPath(
          new URL("./theme/layouts/MySidebar.vue", import.meta.url),
        ),
      },
    },
  },
});

Now whenever VitePress imports VPSidebar, it gets your component instead. The key is matching the exact import path the default theme uses internally.

Phase 3: Handle Isolated Parts First

Before tackling navigation or content routing, I replaced smaller, self-contained pieces:

  • Footer - No dependencies on VitePress internals
  • Background decorations - Pure presentation components
  • Article cards and indexes - Data comes from VitePress loaders, but rendering is independent

Each replacement was a small, testable change. Build, verify, commit, move on.

Phase 4: Replace Navigation

Navigation was the first major system to replace. I created:

  • MyNav.vue - Desktop navigation bar with site title, links, theme toggle
  • MyNavScreen.vue - Mobile drawer menu
  • useNav.ts - Shared composable for menu state

The navigation data comes from VitePress's useData() composable, which provides theme.nav from your config. No need to reinvent data loading.

vue
<script setup>
import { useData } from "vitepress";
const { site, theme, isDark } = useData();
</script>

<template>
  <nav>
    <a v-for="link in theme.nav" :href="link.link">
      {{ link.text }}
    </a>
  </nav>
</template>

Phase 5: Replace Content Routing

The default theme's VPContent component routes to different layouts based on frontmatter. Replacing it meant building my own router:

vue
<script setup>
import { computed } from "vue";
import { useData, Content } from "vitepress";

const { frontmatter } = useData();

const layoutComponents = {
  HomeLayout,
  "article-layout": ArticleLayout,
  "page-layout": PageLayout,
};

const currentLayout = computed(
  () => layoutComponents[frontmatter.value.layout] || null,
);
</script>

<template>
  <component :is="currentLayout" v-if="currentLayout" />
  <div v-else class="vp-doc">
    <Content />
  </div>
</template>

The Content component from VitePress renders your markdown—that's the one piece you always keep.

Phase 6: Extract Markdown Styles

The .vp-doc class provides all the typography, table, code block, and custom container styles. When you remove the default theme, these disappear.

I extracted the essential styles into separate SCSS files:

  • _doc.scss - Tables, header anchors, images, horizontal rules
  • _code.scss - Syntax highlighting, language labels, copy button
  • _custom-blocks.scss - Tip, warning, danger, info containers

The syntax highlighting is handled by Shiki, which injects inline CSS variables. You just need the theme-switching CSS:

scss
.dark .shiki span {
  color: var(--shiki-dark, inherit);
}

html:not(.dark) .shiki span {
  color: var(--shiki-light, inherit);
}

Phase 7: Remove the Default Theme

With all components replaced, I removed the extends declaration:

typescript
// Before
export default {
  extends: DefaultTheme,
  Layout: () => h(Layout),
};

// After
export default {
  Layout: () => h(Layout),
};

This is when you discover what you missed. For me it was:

  • "On This Page" outline - The sticky sidebar showing article headings
  • Previous/Next navigation - Links between articles
  • Layout CSS variables - --vp-nav-height, --vp-sidebar-width, etc.

Each missing feature required building a replacement. The outline component queries the DOM for headings and tracks scroll position. The prev/next links pull from my article data loader.

Results

MetricBeforeAfter
Theme entry point613KB1.3KB
Framework chunk108KB(included in pages)
Initial page load~720KB JS~50KB JS

More importantly, I now understand exactly what my theme does. There's no mystery CSS causing layout shifts, no unused components inflating the bundle, no fighting against default styles.

Recommendations

  1. Don't start from scratch. Extend the default theme first. Get familiar with how VitePress works before replacing things.

  2. Replace incrementally. Each phase should result in a working site. If something breaks, you know exactly what caused it.

  3. Keep VitePress core. The Content component, useData() composable, and data loaders are well-designed. Use them.

  4. Test dark mode constantly. It's easy to forget theme switching when you're focused on layout. Toggle frequently.

  5. Check mobile. The default theme handles responsive design well. Your replacements need to match that quality.

The migration took focused effort across several sessions, but the result is a theme I fully control—and a significantly lighter bundle for visitors.