Creating a Custom Theme
VitePress allows you to create completely custom themes to match your specific design requirements. A custom theme gives you full control over the layout, styling, and functionality of your documentation site.
Theme Structure
Theme Entry File
Create a theme entry file at .vitepress/theme/index.js or .vitepress/theme/index.ts:
.
├─ docs
│ ├─ .vitepress
│ │ ├─ theme
│ │ │ └─ index.js # Theme entry
│ │ └─ config.js
│ └─ index.md
└─ package.json
When VitePress detects a theme entry file, it will use your custom theme instead of the default theme.
Theme Interface
A VitePress theme must implement this interface:
interface Theme {
/**
* Root layout component for every page
* @required
*/
Layout : Component
/**
* Enhance Vue app instance
* @optional
*/
enhanceApp ?: ( ctx : EnhanceAppContext ) => Awaitable < void >
/**
* Extend another theme, calling its `enhanceApp` before ours
* @optional
*/
extends ?: Theme
}
interface EnhanceAppContext {
app : App // Vue app instance
router : Router // VitePress router instance
siteData : Ref < SiteData > // Site-level metadata
}
Basic Custom Theme
Minimal Setup
The simplest possible theme:
.vitepress/theme/index.js
.vitepress/theme/Layout.vue
import Layout from './Layout.vue'
export default {
Layout
}
The <Content /> component renders the compiled Markdown content.
With Type Safety
.vitepress/theme/index.ts
import type { Theme } from 'vitepress'
import Layout from './Layout.vue'
export default {
Layout ,
enhanceApp ({ app , router , siteData }) {
// App enhancements
}
} satisfies Theme
.vitepress/theme/index.js
import Layout from './Layout.vue'
/** @type {import('vitepress').Theme} */
export default {
Layout ,
enhanceApp ({ app , router , siteData }) {
// App enhancements
}
}
Building a Layout Component
Basic Layout
A layout needs at minimum the <Content /> component:
.vitepress/theme/Layout.vue
< script setup >
import { Content } from 'vitepress'
</ script >
< template >
< div class = "layout" >
< header >
< h1 > My Site </ h1 >
</ header >
< main >
< Content />
</ main >
< footer >
< p > © 2024 My Company </ p >
</ footer >
</ div >
</ template >
< style scoped >
.layout {
max-width : 1200 px ;
margin : 0 auto ;
padding : 20 px ;
}
</ style >
Handling 404 Pages
Use the useData() composable to detect 404 pages:
< script setup >
import { useData , Content } from 'vitepress'
const { page } = useData ()
</ script >
< template >
< div class = "layout" >
< div v-if = " page . isNotFound " class = "not-found" >
< h1 > 404 </ h1 >
< p > Page not found </ p >
< a href = "/" > Go home </ a >
</ div >
< Content v-else />
</ div >
</ template >
Multiple Layout Types
Support different layouts via frontmatter:
< script setup >
import { useData , Content } from 'vitepress'
import HomePage from './HomePage.vue'
import DocPage from './DocPage.vue'
import NotFound from './NotFound.vue'
const { page , frontmatter } = useData ()
</ script >
< template >
< NotFound v-if = " page . isNotFound " />
< HomePage v-else-if = " frontmatter . layout === 'home' " />
< DocPage v-else />
</ template >
Then in your Markdown:
---
layout : home
---
# Home Page Content
Using Runtime Data
Available Composables
VitePress provides several composables for accessing runtime data:
useData
useRoute
useRouter
< script setup >
import { useData } from 'vitepress'
const {
site , // Site-level data
page , // Current page data
frontmatter , // Current page frontmatter
params , // Dynamic route params
theme , // Theme config
isDark , // Dark mode state
lang , // Current language
localeIndex , // Current locale index
title , // Page title
description // Page description
} = useData ()
</ script >
< template >
< div >
< h1 > {{ title }} </ h1 >
< p > {{ description }} </ p >
< p > Theme: {{ isDark ? 'Dark' : 'Light' }} </ p >
</ div >
</ template >
< script setup >
import { useRoute } from 'vitepress'
const route = useRoute ()
</ script >
< template >
< div >
< p > Current path: {{ route . path }} </ p >
< p > Component: {{ route . component }} </ p >
</ div >
</ template >
< script setup >
import { useRouter } from 'vitepress'
const router = useRouter ()
function goToPage () {
router . go ( '/guide/' )
}
</ script >
< template >
< button @ click = " goToPage " > Go to Guide </ button >
</ template >
Accessing Page Data
< script setup >
import { useData } from 'vitepress'
const { page , frontmatter } = useData ()
</ script >
< template >
< article >
< h1 > {{ page . title }} </ h1 >
< div class = "meta" >
< span v-if = " frontmatter . author " > By {{ frontmatter . author }} </ span >
< span v-if = " frontmatter . date " > {{ frontmatter . date }} </ span >
< span v-if = " page . lastUpdated " >
Updated: {{ new Date ( page . lastUpdated ). toLocaleDateString () }}
</ span >
</ div >
< Content />
</ article >
</ template >
Enhancing the App
Registering Global Components
.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import MyButton from './components/MyButton.vue'
import MyCard from './components/MyCard.vue'
export default {
extends: DefaultTheme ,
enhanceApp ({ app }) {
app . component ( 'MyButton' , MyButton )
app . component ( 'MyCard' , MyCard )
}
}
Now use them in Markdown:
# My Page
< MyButton > Click me </ MyButton >
< MyCard title = "Card Title" >
Card content
</ MyCard >
Auto-Registering Components
Use Vite’s glob import:
.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
export default {
extends: DefaultTheme ,
enhanceApp ({ app }) {
// Auto-register all components from ./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 )
}
}
}
}
Installing Vue Plugins
.vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import VueGtag from 'vue-gtag'
export default {
extends: DefaultTheme ,
enhanceApp ({ app , router }) {
app . use ( VueGtag , {
config: { id: 'GA_MEASUREMENT_ID' }
}, router )
}
}
Adding Global State
.vitepress/theme/index.js
import { createPinia } from 'pinia'
import DefaultTheme from 'vitepress/theme'
export default {
extends: DefaultTheme ,
enhanceApp ({ app }) {
const pinia = createPinia ()
app . use ( pinia )
}
}
SSR Compatibility
Your theme must be SSR-compatible. Avoid accessing browser-only APIs during SSR.
Conditional Browser Code
onMounted
ClientOnly Component
Dynamic Import
< script setup >
import { onMounted } from 'vue'
onMounted (() => {
// Safe to use browser APIs here
console . log ( window . location . href )
document . title = 'My Page'
})
</ script >
< template >
< ClientOnly >
< BrowserOnlyComponent />
</ ClientOnly >
</ template >
< script setup >
import { defineAsyncComponent } from 'vue'
const BrowserComponent = defineAsyncComponent (() =>
import ( './BrowserComponent.vue' )
)
</ script >
< template >
< ClientOnly >
< BrowserComponent />
</ ClientOnly >
</ template >
Import Guards
if ( typeof window !== 'undefined' ) {
// Browser-only imports
const analytics = await import ( './analytics' )
analytics . init ()
}
Theme Styling
Scoped Styles
< template >
< div class = "custom-layout" >
< Content />
</ div >
</ template >
< style scoped >
.custom-layout {
font-family : 'Inter' , sans-serif ;
line-height : 1.6 ;
}
</ style >
Global Styles
.vitepress/theme/index.js
import Layout from './Layout.vue'
import './styles/global.css'
import './styles/variables.css'
export default {
Layout
}
* {
box-sizing : border-box ;
}
body {
margin : 0 ;
font-family : system-ui , -apple-system , sans-serif ;
}
code {
font-family : 'Monaco' , monospace ;
background : #f5f5f5 ;
padding : 2 px 6 px ;
border-radius : 4 px ;
}
CSS Variables
:root {
--color-primary : #3b82f6 ;
--color-text : #1f2937 ;
--color-background : #ffffff ;
--spacing-unit : 8 px ;
}
.dark {
--color-text : #f9fafb ;
--color-background : #111827 ;
}
Distributing Your Theme
As npm Package
Structure your package:
my-vitepress-theme/
├─ src/
│ ├─ Layout.vue
│ ├─ index.ts
│ └─ components/
├─ package.json
└─ README.md
Export the theme:
import type { Theme } from 'vitepress'
import Layout from './Layout.vue'
export type { ThemeConfig } from './config'
const theme : Theme = {
Layout ,
enhanceApp ({ app }) {
// Theme enhancements
}
}
export default theme
Package.json:
{
"name" : "my-vitepress-theme" ,
"version" : "1.0.0" ,
"type" : "module" ,
"exports" : {
"." : "./src/index.ts" ,
"./config" : "./config.ts"
},
"files" : [ "src" , "config.ts" ],
"peerDependencies" : {
"vitepress" : "^1.0.0" ,
"vue" : "^3.0.0"
}
}
Document usage:
# My VitePress Theme
## Installation
\`\`\` bash
npm install my-vitepress-theme
\`\`\`
## Usage
\`\`\` ts .vitepress/theme/index.ts
import Theme from 'my-vitepress-theme'
export default Theme
\`\`\`
## Configuration
\`\`\` ts .vitepress/config.ts
import { defineConfig } from 'vitepress'
import type { ThemeConfig } from 'my-vitepress-theme'
export default defineConfig < ThemeConfig > ({
themeConfig: {
// Theme-specific options
}
})
\`\`\`
As GitHub Template
Create a template repository with:
vitepress-theme-template/
├─ docs/
│ ├─ .vitepress/
│ │ ├─ theme/
│ │ │ ├─ Layout.vue
│ │ │ ├─ index.ts
│ │ │ └─ components/
│ │ └─ config.ts
│ └─ index.md
├─ package.json
└─ README.md
Users can then use “Use this template” to create their own repository.
Advanced Examples
Blog Theme
.vitepress/theme/Layout.vue
< script setup >
import { useData , useRoute } from 'vitepress'
import { data as posts } from './posts.data'
const { frontmatter , page } = useData ()
const route = useRoute ()
const isBlogPost = route . path . startsWith ( '/blog/' )
</ script >
< template >
< div class = "blog-layout" >
< header >
< nav >
< a href = "/" > Home </ a >
< a href = "/blog/" > Blog </ a >
< a href = "/about/" > About </ a >
</ nav >
</ header >
< main >
< article v-if = " isBlogPost " class = "post" >
< h1 > {{ page . title }} </ h1 >
< div class = "meta" >
< span > {{ frontmatter . author }} </ span >
< time > {{ frontmatter . date }} </ time >
</ div >
< Content />
</ article >
< Content v-else />
</ main >
</ div >
</ template >
.vitepress/theme/Layout.vue
< script setup >
import { useData } from 'vitepress'
import Sidebar from './Sidebar.vue'
import Navbar from './Navbar.vue'
const { theme , page } = useData ()
</ script >
< template >
< div class = "doc-layout" >
< Navbar />
< div class = "container" >
< Sidebar v-if = " theme . sidebar " : items = " theme . sidebar " />
< main class = "content" >
< article >
< h1 > {{ page . title }} </ h1 >
< Content />
</ article >
</ main >
</ div >
</ div >
</ template >
< style scoped >
.container {
display : flex ;
max-width : 1400 px ;
margin : 0 auto ;
}
.content {
flex : 1 ;
padding : 2 rem ;
max-width : 800 px ;
}
</ style >
Complete Theme Example
.vitepress/theme/index.ts
import type { Theme } from 'vitepress'
import Layout from './Layout.vue'
import './styles/vars.css'
import './styles/base.css'
// Components
import Button from './components/Button.vue'
import Card from './components/Card.vue'
import Badge from './components/Badge.vue'
export interface ThemeConfig {
navbar : {
logo ?: string
items : NavItem []
}
sidebar : SidebarConfig
footer ?: {
message ?: string
copyright ?: string
}
}
const theme : Theme = {
Layout ,
enhanceApp ({ app , router , siteData }) {
// Register global components
app . component ( 'Button' , Button )
app . component ( 'Card' , Card )
app . component ( 'Badge' , Badge )
// Add router hooks
router . onBeforeRouteChange = ( to ) => {
console . log ( 'Navigating to' , to )
}
// Global properties
app . config . globalProperties . $myThemeVersion = '1.0.0'
}
}
export default theme
Next Steps
Extending Default Theme Build on top of the default theme instead
Runtime API Explore available composables and utilities