Skip to main content

Extending the Default Theme

The VitePress default theme is highly customizable through CSS variables, component registration, and layout slots. This allows you to build on top of the default theme without creating a theme from scratch.
Before proceeding, make sure you understand how custom themes work.

Basic Setup

Theme Entry File

Create .vitepress/theme/index.js or .vitepress/theme/index.ts:
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import './custom.css'

export default {
  extends: DefaultTheme,
  enhanceApp({ app, router, siteData }) {
    // App enhancements
  }
} satisfies Theme

Customizing CSS

CSS Variables

The default theme uses CSS custom properties for styling. Override them to customize colors, fonts, spacing, and more.
.vitepress/theme/custom.css
:root {
  /* Brand colors */
  --vp-c-brand-1: #646cff;
  --vp-c-brand-2: #747bff;
  --vp-c-brand-3: #535bf2;
  
  /* Background colors */
  --vp-c-bg: #ffffff;
  --vp-c-bg-alt: #f6f6f7;
  --vp-c-bg-elv: #ffffff;
  --vp-c-bg-soft: #f6f6f7;
}

.dark {
  --vp-c-bg: #1b1b1f;
  --vp-c-bg-alt: #161618;
  --vp-c-bg-elv: #202127;
  --vp-c-bg-soft: #202127;
}
See the full list of CSS variables in the VitePress source code.

Custom Styles

Add custom CSS rules:
.vitepress/theme/custom.css
/* Custom heading styles */
.vp-doc h1 {
  border-bottom: 2px solid var(--vp-c-brand-1);
  padding-bottom: 0.5rem;
}

/* Custom link styles */
.vp-doc a {
  text-decoration: underline;
  text-underline-offset: 4px;
}

/* Custom code block styles */
.vp-doc div[class*='language-'] {
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* Custom sidebar styles */
.VPSidebar {
  background: linear-gradient(180deg, 
    var(--vp-c-bg) 0%, 
    var(--vp-c-bg-soft) 100%
  );
}

Using Custom Fonts

Web Fonts

By default, VitePress includes Inter font. To use different fonts:
.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme-without-fonts'
import './fonts.css'

export default DefaultTheme
.vitepress/theme/fonts.css
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');

:root {
  --vp-font-family-base: 'Roboto', sans-serif;
}
Import from vitepress/theme-without-fonts to avoid bundling the default Inter font.

Local Fonts

.vitepress/theme/fonts.css
@font-face {
  font-family: 'CustomFont';
  src: url('./assets/custom-font.woff2') format('woff2');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

:root {
  --vp-font-family-base: 'CustomFont', sans-serif;
}
Preload the font:
.vitepress/config.ts
export default defineConfig({
  transformHead({ assets }) {
    const fontFile = assets.find(file => /custom-font\.\w+\.woff2/.test(file))
    if (fontFile) {
      return [
        [
          'link',
          {
            rel: 'preload',
            href: fontFile,
            as: 'font',
            type: 'font/woff2',
            crossorigin: ''
          }
        ]
      ]
    }
  }
})

Registering Components

Global Components

.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import MyComponent from './components/MyComponent.vue'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    app.component('MyComponent', MyComponent)
  }
}

Layout Slots

The default theme provides layout slots to inject custom content at specific locations.

Available Slots

When layout: 'doc' (default):
  • doc-top - Before doc content
  • doc-bottom - After doc content
  • doc-footer-before - Before doc footer
  • doc-before - Before doc container
  • doc-after - After doc container
  • sidebar-nav-before - Before sidebar nav
  • sidebar-nav-after - After sidebar nav
  • aside-top - Top of aside
  • aside-bottom - Bottom of aside
  • aside-outline-before - Before outline
  • aside-outline-after - After outline
  • aside-ads-before - Before ads
  • aside-ads-after - After ads

Using Slots with Components

.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import MyLayout from './MyLayout.vue'

export default {
  extends: DefaultTheme,
  Layout: MyLayout
}
.vitepress/theme/MyLayout.vue
<script setup>
import DefaultTheme from 'vitepress/theme'
import CustomSidebar from './CustomSidebar.vue'
import Banner from './Banner.vue'

const { Layout } = DefaultTheme
</script>

<template>
  <Layout>
    <template #doc-top>
      <Banner />
    </template>
    
    <template #sidebar-nav-before>
      <CustomSidebar />
    </template>
    
    <template #aside-outline-after>
      <div class="custom-toc-footer">
        <a href="#">Back to top</a>
      </div>
    </template>
  </Layout>
</template>

Slot Examples

Add Banner to All Docs

.vitepress/theme/MyLayout.vue
<script setup>
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
</script>

<template>
  <Layout>
    <template #doc-top>
      <div class="banner">
        <p>📢 This is a beta version. <a href="/feedback">Give feedback</a></p>
      </div>
    </template>
  </Layout>
</template>

<style scoped>
.banner {
  background: var(--vp-c-brand-soft);
  padding: 12px 24px;
  text-align: center;
  border-radius: 8px;
  margin-bottom: 24px;
}
</style>

Add Contributors List

.vitepress/theme/MyLayout.vue
<script setup>
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import Contributors from './Contributors.vue'

const { Layout } = DefaultTheme
const { frontmatter } = useData()
</script>

<template>
  <Layout>
    <template #doc-after>
      <Contributors 
        v-if="frontmatter.contributors" 
        :contributors="frontmatter.contributors" 
      />
    </template>
  </Layout>
</template>
.vitepress/theme/MyLayout.vue
<script setup>
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
</script>

<template>
  <Layout>
    <template #sidebar-nav-after>
      <div class="sidebar-footer">
        <h3>Need Help?</h3>
        <ul>
          <li><a href="/support">Support</a></li>
          <li><a href="/discord">Discord</a></li>
          <li><a href="/github">GitHub</a></li>
        </ul>
      </div>
    </template>
  </Layout>
</template>

<style scoped>
.sidebar-footer {
  padding: 16px;
  border-top: 1px solid var(--vp-c-divider);
  margin-top: 16px;
}
</style>

Overriding Components

Replace default theme components using Vite aliases:
.vitepress/config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vitepress'

export default defineConfig({
  vite: {
    resolve: {
      alias: [
        {
          find: /^.*\/VPNavBar\.vue$/,
          replacement: fileURLToPath(
            new URL('./theme/components/CustomNavBar.vue', import.meta.url)
          )
        },
        {
          find: /^.*\/VPFooter\.vue$/,
          replacement: fileURLToPath(
            new URL('./theme/components/CustomFooter.vue', import.meta.url)
          )
        }
      ]
    }
  }
})
Component names may change between minor releases. Check the source code for current names.

View Transitions

On Appearance Toggle

Add smooth transitions when toggling dark mode:
.vitepress/theme/Layout.vue
<script setup lang="ts">
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import { nextTick, provide } from 'vue'

const { isDark } = useData()

const enableTransitions = () =>
  'startViewTransition' in document &&
  window.matchMedia('(prefers-reduced-motion: no-preference)').matches

provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
  if (!enableTransitions()) {
    isDark.value = !isDark.value
    return
  }

  const clipPath = [
    `circle(0px at ${x}px ${y}px)`,
    `circle(${Math.hypot(
      Math.max(x, innerWidth - x),
      Math.max(y, innerHeight - y)
    )}px at ${x}px ${y}px)`
  ]

  await document.startViewTransition(async () => {
    isDark.value = !isDark.value
    await nextTick()
  }).ready

  document.documentElement.animate(
    { clipPath: isDark.value ? clipPath.reverse() : clipPath },
    {
      duration: 300,
      easing: 'ease-in',
      pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`
    }
  )
})
</script>

<template>
  <DefaultTheme.Layout />
</template>

<style>
::view-transition-old(root),
::view-transition-new(root) {
  animation: none;
  mix-blend-mode: normal;
}

::view-transition-old(root),
.dark::view-transition-new(root) {
  z-index: 1;
}

::view-transition-new(root),
.dark::view-transition-old(root) {
  z-index: 9999;
}
</style>

Complete Example

.vitepress/theme/index.js
import { h } from 'vue'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'

// Custom CSS
import './custom.css'
import './fonts.css'

// Custom components
import Banner from './components/Banner.vue'
import Contributors from './components/Contributors.vue'
import CustomButton from './components/CustomButton.vue'
import Analytics from './components/Analytics.vue'

export default {
  extends: DefaultTheme,
  
  Layout() {
    return h(DefaultTheme.Layout, null, {
      'doc-top': () => h(Banner),
      'doc-after': () => h(Contributors),
      'layout-bottom': () => h(Analytics)
    })
  },
  
  enhanceApp({ app, router, siteData }) {
    // Register global components
    app.component('CustomButton', CustomButton)
    
    // Add router hooks
    router.onAfterRouteChanged = (to) => {
      // Track page views
      if (typeof window !== 'undefined' && window.gtag) {
        window.gtag('event', 'page_view', {
          page_path: to
        })
      }
    }
    
    // Global error handler
    app.config.errorHandler = (err, instance, info) => {
      console.error('App error:', err, info)
    }
  }
} satisfies Theme

Next Steps

Theme Configuration

Configure default theme options

Custom Theme

Create a completely custom theme

Build docs developers (and LLMs) love