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.
Basic Setup
Theme Entry File
Create .vitepress/theme/index.js or .vitepress/theme/index.ts:
.vitepress/theme/index.ts
.vitepress/theme/index.js
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.
Colors
Typography
Layout
Components
.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 ;
}
.vitepress/theme/custom.css
:root {
/* Font families */
--vp-font-family-base : 'Inter' , system-ui , sans-serif ;
--vp-font-family-mono : 'Monaco' , monospace ;
/* Font sizes */
--vp-font-size-base : 16 px ;
--vp-font-size-small : 14 px ;
--vp-font-size-large : 18 px ;
/* Line heights */
--vp-line-height-base : 1.7 ;
--vp-line-height-heading : 1.2 ;
}
.vitepress/theme/custom.css
:root {
/* Layout dimensions */
--vp-layout-max-width : 1440 px ;
--vp-sidebar-width-small : 272 px ;
--vp-sidebar-width-mobile : 320 px ;
/* Spacing */
--vp-spacing-sm : 8 px ;
--vp-spacing-md : 16 px ;
--vp-spacing-lg : 24 px ;
--vp-spacing-xl : 32 px ;
}
.vitepress/theme/custom.css
:root {
/* Buttons */
--vp-button-brand-bg : var ( --vp-c-brand-1 );
--vp-button-brand-hover-bg : var ( --vp-c-brand-2 );
/* Code blocks */
--vp-code-font-size : 14 px ;
--vp-code-line-height : 1.5 ;
--vp-code-block-bg : #f6f6f7 ;
/* Links */
--vp-c-link : var ( --vp-c-brand-1 );
--vp-c-link-hover : var ( --vp-c-brand-2 );
}
Custom Styles
Add custom CSS rules:
.vitepress/theme/custom.css
/* Custom heading styles */
.vp-doc h1 {
border-bottom : 2 px solid var ( --vp-c-brand-1 );
padding-bottom : 0.5 rem ;
}
/* Custom link styles */
.vp-doc a {
text-decoration : underline ;
text-underline-offset : 4 px ;
}
/* Custom code block styles */
.vp-doc div [ class *= 'language-' ] {
border-radius : 8 px ;
box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.1 );
}
/* Custom sidebar styles */
.VPSidebar {
background : linear-gradient ( 180 deg ,
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:
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
Single Component
Multiple Components
Auto-Register
.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 )
}
}
.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import Button from './components/Button.vue'
import Card from './components/Card.vue'
import Badge from './components/Badge.vue'
export default {
extends: DefaultTheme ,
enhanceApp ({ app }) {
app . component ( 'Button' , Button )
app . component ( 'Card' , Card )
app . component ( 'Badge' , Badge )
}
}
.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
export default {
extends: DefaultTheme ,
enhanceApp ({ app }) {
// Auto-register all components
const components = import . meta . glob ( './components/**/*.vue' , {
eager: true
})
for ( const path in components ) {
const component = components [ path ]
const name = path . match ( / \/ ( [ ^ \/ ] + ) \. vue $ / )?.[ 1 ]
if ( name ) {
app . component ( name , component . default )
}
}
}
}
Layout Slots
The default theme provides layout slots to inject custom content at specific locations.
Available Slots
Doc Layout
Home Layout
Page Layout
Global 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
When layout: 'home':
home-hero-before - Before hero section
home-hero-info-before - Before hero info
home-hero-info - Hero info
home-hero-info-after - After hero info
home-hero-actions-after - After hero actions
home-hero-image - Hero image
home-hero-after - After hero
home-features-before - Before features
home-features-after - After features
When layout: 'page':
page-top - Top of page
page-bottom - Bottom of page
Always available:
layout-top - Top of layout
layout-bottom - Bottom of layout
nav-bar-title-before - Before nav title
nav-bar-title-after - After nav title
nav-bar-content-before - Before nav content
nav-bar-content-after - After nav content
nav-screen-content-before - Before mobile nav
nav-screen-content-after - After mobile nav
not-found - 404 page
Using Slots with Components
With Layout Wrapper
With Render Function
.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 >
.vitepress/theme/index.js
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import Banner from './components/Banner.vue'
import TableOfContents from './components/TableOfContents.vue'
export default {
extends: DefaultTheme ,
Layout () {
return h ( DefaultTheme . Layout , null , {
'doc-top' : () => h ( Banner ),
'aside-outline-before' : () => h ( TableOfContents )
})
}
}
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 : 12 px 24 px ;
text-align : center ;
border-radius : 8 px ;
margin-bottom : 24 px ;
}
</ 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 : 16 px ;
border-top : 1 px solid var ( --vp-c-divider );
margin-top : 16 px ;
}
</ style >
Overriding Components
Replace default theme components using Vite aliases:
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