VitePress Custom Theme
Introduction
VitePress offers a flexible theming system that enables you to fully customize your documentation site's appearance and behavior. This guide will walk you through the process of creating and customizing themes, from basic style changes to advanced component customization.
Theme Basics
Directory Structure
The foundation of a VitePress theme is the .vitepress/theme
directory. Create this structure in your project:
.vitepress/
├── theme/
│ ├── index.js # Theme entry point
│ ├── Layout.vue # Optional custom layout
│ ├── components/ # Custom Vue components
│ └── styles/ # CSS styles
└── config.js # VitePress configuration
Creating a Basic Theme
- Set up the theme entry file:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './styles/custom.css'
export default {
...DefaultTheme,
enhanceApp({ app }) {
// Register components or add app-level enhancements
}
}
- Add custom styles:
/* .vitepress/theme/styles/custom.css */
:root {
/* Brand colors */
--vp-c-brand: #3a70f0;
--vp-c-brand-light: #5c8af2;
--vp-c-brand-dark: #2860e0;
/* Typography */
--vp-font-family-base: 'Inter', sans-serif;
--vp-font-family-mono: 'Fira Code', monospace;
}
/* Custom component styles */
.custom-block {
padding: 16px;
border-radius: 8px;
margin: 16px 0;
background-color: var(--vp-c-bg-soft);
}
Component Customization
Extending Default Components
VitePress allows you to extend or override default theme components:
<!-- .vitepress/theme/components/VPNavBar.vue -->
<script setup>
import { VPNavBar } from 'vitepress/theme'
</script>
<template>
<VPNavBar>
<template #nav-bar-content-before>
<div class="custom-nav-content">
<!-- Your custom navigation content -->
<a href="/special-page/">Special Page</a>
</div>
</template>
</VPNavBar>
</template>
Register in your theme:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import VPNavBar from './components/VPNavBar.vue'
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('VPNavBar', VPNavBar)
}
}
Creating Custom Components
Build reusable components for your documentation:
<!-- .vitepress/theme/components/FeatureComparison.vue -->
<script setup>
const props = defineProps({
features: Array,
products: Array
})
</script>
<template>
<div class="feature-comparison">
<table>
<thead>
<tr>
<th>Feature</th>
<th v-for="product in products" :key="product.name">{{ product.name }}</th>
</tr>
</thead>
<tbody>
<tr v-for="feature in features" :key="feature.name">
<td>{{ feature.name }}</td>
<td v-for="product in products" :key="product.name">
{{ product.features[feature.id] ? '✅' : '❌' }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
Use in Markdown:
<FeatureComparison
:features="[
{ id: 'feature1', name: 'Feature 1' },
{ id: 'feature2', name: 'Feature 2' }
]"
:products="[
{ name: 'Basic', features: { feature1: true, feature2: false } },
{ name: 'Pro', features: { feature1: true, feature2: true } }
]"
/>
Custom Layouts
Extending the Default Layout
Create a layout that extends the default one:
<!-- .vitepress/theme/Layout.vue -->
<script setup>
import DefaultTheme from 'vitepress/theme'
import { useData } from 'vitepress'
const { Layout } = DefaultTheme
const { frontmatter } = useData()
</script>
<template>
<Layout>
<template #layout-top v-if="frontmatter.customBanner">
<div class="custom-banner">
{{ frontmatter.customBanner }}
</div>
</template>
</Layout>
</template>
<style scoped>
.custom-banner {
padding: 1rem;
background-color: var(--vp-c-brand);
color: white;
text-align: center;
}
</style>
Use in your Markdown:
---
customBanner: "Special announcement for this page!"
---
# Page with Custom Banner
Creating a Fully Custom Layout
For complete control, create a custom layout from scratch:
<!-- .vitepress/theme/CustomLayout.vue -->
<script setup>
import { useData, useRoute } from 'vitepress'
import { computed } from 'vue'
const { site, page } = useData()
const route = useRoute()
const showSidebar = computed(() => {
return page.value.frontmatter.sidebar !== false &&
route.path !== '/' &&
!page.value.frontmatter.home
})
</script>
<template>
<div class="custom-layout">
<header>
<div class="container">
<h1>{{ site.title }}</h1>
<nav>
<a v-for="item in site.themeConfig.nav"
:key="item.text"
:href="item.link">
{{ item.text }}
</a>
</nav>
</div>
</header>
<div class="container main">
<aside v-if="showSidebar" class="sidebar">
<!-- Sidebar content -->
</aside>
<main class="content">
<h1>{{ page.title }}</h1>
<Content />
</main>
</div>
<footer>
<div class="container">
© {{ new Date().getFullYear() }} {{ site.title }}
</div>
</footer>
</div>
</template>
Theme Styling
Color Customization
VitePress uses CSS variables for theming. Customize colors by overriding these variables:
/* .vitepress/theme/styles/custom.css */
:root {
/* Light theme */
--vp-c-brand: #3eaf7c;
--vp-c-brand-light: #4abf8a;
--vp-c-brand-dark: #369f6e;
--vp-c-bg: #ffffff;
--vp-c-bg-soft: #f9f9f9;
--vp-c-bg-mute: #f1f1f1;
--vp-c-text-1: rgba(60, 60, 60, 1);
--vp-c-text-2: rgba(60, 60, 60, 0.75);
}
.dark {
/* Dark theme */
--vp-c-bg: #111827;
--vp-c-bg-soft: #1f2937;
--vp-c-bg-mute: #374151;
--vp-c-text-1: rgba(255, 255, 255, 0.87);
--vp-c-text-2: rgba(255, 255, 255, 0.6);
}
Creating a Color System
For larger projects, create a comprehensive color system:
:root {
/* Primary palette */
--color-primary-50: #ecfdf5;
--color-primary-100: #d1fae5;
--color-primary-500: #10b981;
--color-primary-900: #064e3b;
/* Map to VitePress variables */
--vp-c-brand: var(--color-primary-500);
--vp-c-brand-light: var(--color-primary-400);
--vp-c-brand-dark: var(--color-primary-600);
}
Common Theme Components
Custom Home Page
Create a custom home page component:
<!-- .vitepress/theme/components/HomePage.vue -->
<script setup>
import { useData } from 'vitepress'
const { frontmatter } = useData()
</script>
<template>
<div class="home">
<section class="hero">
<h1>{{ frontmatter.heroTitle }}</h1>
<p>{{ frontmatter.heroTagline }}</p>
<div class="actions">
<a class="primary" :href="frontmatter.actionLink">
{{ frontmatter.actionText }}
</a>
<a class="secondary" :href="frontmatter.altActionLink">
{{ frontmatter.altActionText }}
</a>
</div>
</section>
<section class="features">
<div v-for="feature in frontmatter.features"
:key="feature.title"
class="feature">
<div class="icon" v-html="feature.icon"></div>
<h2>{{ feature.title }}</h2>
<p>{{ feature.details }}</p>
</div>
</section>
</div>
</template>
<style scoped>
.home {
padding: 3rem 1.5rem;
max-width: 1200px;
margin: 0 auto;
}
.hero {
text-align: center;
margin-bottom: 3rem;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
.actions {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
}
.actions a {
padding: 0.75rem 1.5rem;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
}
.actions a.primary {
background-color: var(--vp-c-brand);
color: white;
}
.actions a.secondary {
background-color: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin-top: 4rem;
}
.feature {
padding: 1.5rem;
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
transition: transform 0.2s;
}
.feature:hover {
transform: translateY(-5px);
}
.feature .icon {
font-size: 2rem;
color: var(--vp-c-brand);
margin-bottom: 1rem;
}
</style>
Use in Markdown:
---
layout: home
heroTitle: My Documentation
heroTagline: Clear, concise, and comprehensive
actionText: Get Started
actionLink: /guide/
altActionText: Learn More
altActionLink: /about/
features:
- title: Feature One
details: Description of feature one
icon: 📚
- title: Feature Two
details: Description of feature two
icon: 🚀
---
<HomePage />
Custom Navigation
Create a responsive navigation component:
<!-- .vitepress/theme/components/CustomNav.vue -->
<script setup>
import { useData } from 'vitepress'
import { ref } from 'vue'
const { site, theme } = useData()
const isMenuOpen = ref(false)
</script>
<template>
<nav class="custom-nav">
<div class="container">
<a href="/" class="logo">{{ site.title }}</a>
<div class="desktop-links">
<a v-for="item in theme.nav"
:key="item.text"
:href="item.link">
{{ item.text }}
</a>
</div>
<button class="menu-toggle" @click="isMenuOpen = !isMenuOpen">
<span></span>
</button>
</div>
<div class="mobile-links" :class="{ open: isMenuOpen }">
<a v-for="item in theme.nav"
:key="item.text"
:href="item.link"
@click="isMenuOpen = false">
{{ item.text }}
</a>
</div>
</nav>
</template>
Advanced Theme Customization
Dark Mode Toggle
Create a dark mode toggle component:
<!-- .vitepress/theme/components/DarkModeToggle.vue -->
<script setup>
import { useData } from 'vitepress'
import { ref, watch, onMounted } from 'vue'
const { isDark } = useData()
const darkMode = ref(false)
watch(isDark, (newVal) => {
darkMode.value = newVal
})
function toggleDarkMode() {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
localStorage.setItem('vitepress-theme-appearance', isDark.value ? 'dark' : 'light')
}
onMounted(() => {
darkMode.value = isDark.value
})
</script>
<template>
<button class="dark-mode-toggle" @click="toggleDarkMode" :title="darkMode ? 'Switch to light mode' : 'Switch to dark mode'">
<svg v-if="darkMode" class="sun-icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM11 1H13V4H11V1ZM11 20H13V23H11V20ZM3.51472 4.92893L4.92893 3.51472L7.05025 5.63604L5.63604 7.05025L3.51472 4.92893ZM16.9497 18.364L18.364 16.9497L20.4853 19.0711L19.0711 20.4853L16.9497 18.364ZM19.0711 3.51472L20.4853 4.92893L18.364 7.05025L16.9497 5.63604L19.0711 3.51472ZM5.63604 16.9497L7.05025 18.364L4.92893 20.4853L3.51472 19.0711L5.63604 16.9497ZM23 11V13H20V11H23ZM4 11V13H1V11H4Z"></path>
</svg>
<svg v-else class="moon-icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M10 7C10 10.866 13.134 14 17 14C18.9584 14 20.729 13.1957 21.9995 11.8995C22 11.933 22 11.9665 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C12.0335 2 12.067 2 12.1005 2.00049C10.8043 3.27098 10 5.04157 10 7ZM4 12C4 16.4183 7.58172 20 12 20C15.0583 20 17.7158 18.2839 19.062 15.7621C18.3945 15.9187 17.7035 16 17 16C12.0294 16 8 11.9706 8 7C8 6.29648 8.08133 5.60547 8.2379 4.938C5.71611 6.28423 4 8.9417 4 12Z"></path>
</svg>
</button>
</template>
<style scoped>
.dark-mode-toggle {
background: none;
border: none;
cursor: pointer;
padding: 8px;
color: var(--vp-c-text-1);
transition: color 0.2s;
}
.dark-mode-toggle:hover {
color: var(--vp-c-brand);
}
.sun-icon, .moon-icon {
width: 24px;
height: 24px;
}
</style>
Register it in your theme:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import DarkModeToggle from './components/DarkModeToggle.vue'
import './styles/custom.css'
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('DarkModeToggle', DarkModeToggle)
}
}
Custom Search
Customize the search component:
<!-- .vitepress/theme/components/CustomSearch.vue -->
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useData, useRouter } from 'vitepress'
const { theme } = useData()
const router = useRouter()
const query = ref('')
const focused = ref(false)
const results = ref([])
const selectedIndex = ref(0)
const searchIndex = ref(null)
const hasResults = computed(() => results.value.length > 0)
async function initSearchIndex() {
try {
// This is a simplified example. In a real implementation, you would load your search index
searchIndex.value = {
search(query) {
// Simple search implementation
const pages = theme.value.pages || []
return pages.filter(page =>
page.title.toLowerCase().includes(query.toLowerCase()) ||
page.text.toLowerCase().includes(query.toLowerCase())
).map(page => ({
title: page.title,
link: page.link,
excerpt: page.text.substring(0, 100) + '...'
}))
}
}
} catch (err) {
console.error('Failed to load search index:', err)
}
}
function search() {
if (!searchIndex.value || !query.value.trim()) {
results.value = []
return
}
results.value = searchIndex.value.search(query.value)
selectedIndex.value = 0
}
function handleKeyDown(e) {
if (e.key === 'ArrowDown') {
e.preventDefault()
selectedIndex.value = (selectedIndex.value + 1) % results.value.length
} else if (e.key === 'ArrowUp') {
e.preventDefault()
selectedIndex.value = (selectedIndex.value - 1 + results.value.length) % results.value.length
} else if (e.key === 'Enter' && results.value[selectedIndex.value]) {
e.preventDefault()
selectResult(results.value[selectedIndex.value])
} else if (e.key === 'Escape') {
e.preventDefault()
focused.value = false
}
}
function selectResult(result) {
router.go(result.link)
query.value = ''
results.value = []
focused.value = false
}
function handleFocus() {
focused.value = true
}
function handleBlur() {
// Delay to allow click on results
setTimeout(() => {
focused.value = false
}, 200)
}
onMounted(() => {
initSearchIndex()
})
</script>
<template>
<div class="custom-search" :class="{ focused }">
<div class="search-input-container">
<input
type="text"
class="search-input"
placeholder="Search documentation..."
v-model="query"
@input="search"
@focus="handleFocus"
@blur="handleBlur"
@keydown="handleKeyDown"
/>
<svg class="search-icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path>
</svg>
</div>
<div v-if="focused && hasResults" class="search-results">
<div
v-for="(result, i) in results"
:key="i"
class="search-result"
:class="{ selected: i === selectedIndex }"
@mousedown="selectResult(result)"
>
<div class="result-title">{{ result.title }}</div>
<div class="result-excerpt">{{ result.excerpt }}</div>
</div>
</div>
<div v-if="focused && query && !hasResults" class="search-results">
<div class="no-results">No results found for "{{ query }}"</div>
</div>
</div>
</template>
<style scoped>
.custom-search {
position: relative;
width: 100%;
max-width: 300px;
}
.search-input-container {
position: relative;
}
.search-input {
width: 100%;
padding: 8px 12px 8px 36px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.9em;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-input:focus {
outline: none;
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px rgba(var(--vp-c-brand-rgb), 0.1);
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: var(--vp-c-text-3);
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 8px;
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
max-height: 400px;
overflow-y: auto;
z-index: 100;
}
.search-result {
padding: 12px;
cursor: pointer;
border-bottom: 1px solid var(--vp-c-divider);
}
.search-result:last-child {
border-bottom: none;
}
.search-result.selected,
.search-result:hover {
background-color: var(--vp-c-bg-soft);
}
.result-title {
font-weight: 500;
margin-bottom: 4px;
}
.result-excerpt {
font-size: 0.85em;
color: var(--vp-c-text-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.no-results {
padding: 12px;
color: var(--vp-c-text-2);
font-style: italic;
}
</style>
Internationalization Support
Create a language switcher component:
<!-- .vitepress/theme/components/LanguageSwitcher.vue -->
<script setup>
import { useData } from 'vitepress'
import { computed } from 'vue'
const { site, page } = useData()
const locales = computed(() => {
return site.value.locales || {}
})
const currentPath = computed(() => page.value.relativePath)
const availableLocales = computed(() => {
const localeKeys = Object.keys(locales.value).filter(key => key !== '/')
return localeKeys.map(key => {
const locale = locales.value[key]
const path = `${key}${currentPath.value}`
return {
label: locale.label || key.replace(/\//g, ''),
link: path
}
})
})
</script>
<template>
<div class="language-switcher" v-if="availableLocales.length > 0">
<div class="current-language">
<svg class="language-icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"></path>
</svg>
<span>{{ site.value.locales['/']?.label || 'English' }}</span>
<svg class="dropdown-icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M7 10l5 5 5-5z"></path>
</svg>
</div>
<div class="language-dropdown">
<a v-for="locale in availableLocales" :key="locale.link" :href="locale.link" class="language-option">
{{ locale.label }}
</a>
</div>
</div>
</template>
<style scoped>
.language-switcher {
position: relative;
display: inline-block;
}
.current-language {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
}
.current-language:hover {
background-color: var(--vp-c-bg-soft);
}
.language-icon, .dropdown-icon {
width: 16px;
height: 16px;
color: var(--vp-c-text-2);
}
.language-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background-color: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 120px;
z-index: 100;
display: none;
}
.language-switcher:hover .language-dropdown {
display: block;
}
.language-option {
display: block;
padding: 8px 12px;
color: var(--vp-c-text-1);
text-decoration: none;
transition: background-color 0.2s;
}
.language-option:hover {
background-color: var(--vp-c-bg-soft);
}
</style>
Performance Optimization
Code Splitting
Optimize your theme with code splitting:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './styles/custom.css'
import { defineAsyncComponent } from 'vue'
// Async load components for better performance
const HomePage = defineAsyncComponent(() => import('./components/HomePage.vue'))
const FeatureComparison = defineAsyncComponent(() => import('./components/FeatureComparison.vue'))
const DarkModeToggle = defineAsyncComponent(() => import('./components/DarkModeToggle.vue'))
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('HomePage', HomePage)
app.component('FeatureComparison', FeatureComparison)
app.component('DarkModeToggle', DarkModeToggle)
}
}
Optimizing CSS
Use CSS variables for better theme customization:
/* .vitepress/theme/styles/custom.css */
:root {
/* Base colors */
--color-white: #ffffff;
--color-black: #000000;
/* Gray scale */
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-300: #d1d5db;
--color-gray-400: #9ca3af;
--color-gray-500: #6b7280;
--color-gray-600: #4b5563;
--color-gray-700: #374151;
--color-gray-800: #1f2937;
--color-gray-900: #111827;
/* Brand colors */
--color-brand-50: #ecfdf5;
--color-brand-100: #d1fae5;
--color-brand-200: #a7f3d0;
--color-brand-300: #6ee7b7;
--color-brand-400: #34d399;
--color-brand-500: #10b981;
--color-brand-600: #059669;
--color-brand-700: #047857;
--color-brand-800: #065f46;
--color-brand-900: #064e3b;
/* Map to VitePress variables */
--vp-c-brand: var(--color-brand-600);
--vp-c-brand-light: var(--color-brand-500);
--vp-c-brand-lighter: var(--color-brand-400);
--vp-c-brand-dark: var(--color-brand-700);
--vp-c-brand-darker: var(--color-brand-800);
--vp-c-text-1: var(--color-gray-900);
--vp-c-text-2: var(--color-gray-700);
--vp-c-text-3: var(--color-gray-500);
--vp-c-bg: var(--color-white);
--vp-c-bg-soft: var(--color-gray-50);
--vp-c-bg-mute: var(--color-gray-100);
--vp-c-divider: var(--color-gray-200);
--vp-c-border: var(--color-gray-200);
}
.dark {
--vp-c-text-1: var(--color-gray-50);
--vp-c-text-2: var(--color-gray-300);
--vp-c-text-3: var(--color-gray-400);
--vp-c-bg: var(--color-gray-900);
--vp-c-bg-soft: var(--color-gray-800);
--vp-c-bg-mute: var(--color-gray-700);
--vp-c-divider: var(--color-gray-700);
--vp-c-border: var(--color-gray-700);
}
Best Practices
Theme Organization
Organize your theme files effectively:
.vitepress/
├── theme/
│ ├── index.js # Theme entry file
│ ├── Layout.vue # Main layout
│ ├── components/ # UI components
│ │ ├── global/ # Global components
│ │ ├── home/ # Home page components
│ │ └── custom/ # Custom components
│ ├── composables/ # Vue composables
│ │ ├── useNavigation.js
│ │ └── useTheme.js
│ ├── styles/ # CSS styles
│ │ ├── base.css # Base styles
│ │ ├── variables.css # CSS variables
│ │ ├── layout.css # Layout styles
│ │ └── custom.css # Custom styles
│ └── utils/ # Utility functions
│ ├── format.js
│ └── helpers.js
└── config.js # VitePress configuration
Component Design Principles
Reusability: Design components to be reusable across different parts of your documentation.
Composability: Create small, focused components that can be composed together.
Accessibility: Ensure your components are accessible to all users.
Responsiveness: Design components to work well on all screen sizes.
Performance: Optimize components for performance, using techniques like lazy loading.
Theme Configuration
Create a theme configuration file:
// .vitepress/theme/config.js
export default {
colors: {
primary: '#10b981',
secondary: '#6366f1',
accent: '#f59e0b',
error: '#ef4444',
success: '#22c55e',
warning: '#f59e0b',
info: '#3b82f6'
},
fonts: {
base: 'Inter, system-ui, sans-serif',
mono: 'JetBrains Mono, monospace',
headings: 'Inter, system-ui, sans-serif'
},
layout: {
maxWidth: '1200px',
contentWidth: '800px'
},
features: {
darkMode: true,
search: true,
editLink: true,
lastUpdated: true
}
}
Use it in your theme:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './styles/custom.css'
import themeConfig from './config'
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.provide('themeConfig', themeConfig)
}
}
Frequently Asked Questions
How do I add custom fonts to my theme?
Add custom fonts using CSS:
/* .vitepress/theme/styles/fonts.css */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/CustomFont.woff2') format('woff2'),
url('/fonts/CustomFont.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
:root {
--vp-font-family-base: 'CustomFont', system-ui, sans-serif;
--vp-font-family-mono: 'JetBrains Mono', monospace;
}
Import it in your theme:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './styles/fonts.css'
import './styles/custom.css'
export default {
...DefaultTheme
}
How do I add custom layouts for specific pages?
Use frontmatter to specify custom layouts:
---
layout: custom-layout
---
# Page with Custom Layout
Create the custom layout component:
<!-- .vitepress/theme/layouts/CustomLayout.vue -->
<script setup>
import { useData } from 'vitepress'
const { frontmatter } = useData()
</script>
<template>
<div class="custom-layout">
<header class="custom-header">
<h1>{{ frontmatter.title }}</h1>
</header>
<main class="custom-content">
<Content />
</main>
<footer class="custom-footer">
Custom Footer
</footer>
</div>
</template>
Register it in your theme:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './layouts/CustomLayout.vue'
import './styles/custom.css'
export default {
...DefaultTheme,
Layout(props) {
if (props.frontmatter.layout === 'custom-layout') {
return h(CustomLayout, props)
}
return h(DefaultTheme.Layout, props)
}
}
How do I add analytics to my VitePress site?
Add analytics using a plugin:
// .
# Custom Theme
## Overview
VitePress provides a powerful theming system that allows you to customize the appearance and behavior of your documentation site. This guide covers everything from basic theme customization to creating a completely custom theme.
## Getting Started with Theme Customization
### Theme Directory Structure
To customize the VitePress theme, create a `.vitepress/theme` directory in your project with the following structure:
.vitepress/ ├── theme/ │ ├── index.js # Theme entry file │ ├── Layout.vue # Optional custom layout │ ├── components/ # Custom components │ └── styles/ # Custom styles └── config.js # VitePress configuration
### Basic Theme Setup
Create a theme entry file at `.vitepress/theme/index.js`:
```js
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './styles/custom.css'
export default {
...DefaultTheme,
enhanceApp({ app, router, siteData }) {
// Register custom global components or perform other app-level enhancements
}
}
Adding Custom CSS
Create a CSS file at .vitepress/theme/styles/custom.css
:
/* .vitepress/theme/styles/custom.css */
:root {
--vp-c-brand: #646cff;
--vp-c-brand-light: #747bff;
--vp-c-brand-lighter: #9499ff;
--vp-c-brand-dark: #535bf2;
--vp-c-brand-darker: #454ce1;
}
/* Custom CSS rules */
.custom-block {
padding: 16px;
border-radius: 8px;
margin: 16px 0;
}
Customizing Theme Components
Overriding Default Components
You can override default theme components by creating components with the same name in your theme directory:
<!-- .vitepress/theme/components/VPNavBar.vue -->
<script setup>
import { VPNavBar } from 'vitepress/theme'
</script>
<template>
<VPNavBar>
<template #nav-bar-content-before>
<div class="custom-nav-content">
Custom Navigation Content
</div>
</template>
</VPNavBar>
</template>
<style scoped>
.custom-nav-content {
margin-right: 12px;
font-weight: 500;
}
</style>
Register the component in your theme entry file:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './styles/custom.css'
import VPNavBar from './components/VPNavBar.vue'
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('VPNavBar', VPNavBar)
}
}
Adding Custom Components
Create custom components to enhance your documentation:
<!-- .vitepress/theme/components/FeatureComparison.vue -->
<script setup>
const props = defineProps({
features: {
type: Array,
required: true
},
products: {
type: Array,
required: true
}
})
</script>
<template>
<div class="feature-comparison">
<table>
<thead>
<tr>
<th>Feature</th>
<th v-for="product in products" :key="product.name">
{{ product.name }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="feature in features" :key="feature.name">
<td>{{ feature.name }}</td>
<td v-for="product in products" :key="product.name">
{{ product.features[feature.id] ? '✅' : '❌' }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped>
.feature-comparison {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
border: 1px solid var(--vp-c-divider);
text-align: center;
}
th {
background-color: var(--vp-c-bg-soft);
}
</style>
Register and use the component:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './styles/custom.css'
import FeatureComparison from './components/FeatureComparison.vue'
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('FeatureComparison', FeatureComparison)
}
}
Use it in your Markdown:
<FeatureComparison
:features="[
{ id: 'feature1', name: 'Feature 1' },
{ id: 'feature2', name: 'Feature 2' },
{ id: 'feature3', name: 'Feature 3' }
]"
:products="[
{ name: 'Product A', features: { feature1: true, feature2: true, feature3: false } },
{ name: 'Product B', features: { feature1: true, feature2: false, feature3: true } }
]"
/>
Creating a Custom Layout
Basic Custom Layout
Create a custom layout component:
<!-- .vitepress/theme/Layout.vue -->
<script setup>
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
const { frontmatter } = useData()
</script>
<template>
<Layout>
<template #layout-top v-if="frontmatter.customLayout">
<div class="custom-layout-banner">
{{ frontmatter.customLayoutBanner }}
</div>
</template>
</Layout>
</template>
<style scoped>
.custom-layout-banner {
padding: 24px;
background-color: var(--vp-c-brand);
color: white;
text-align: center;
font-size: 1.2em;
font-weight: 500;
}
</style>
Use it in your theme entry file:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'
import './styles/custom.css'
export default {
...DefaultTheme,
Layout
}
Use the custom layout in your Markdown frontmatter:
---
customLayout: true
customLayoutBanner: "This page uses a custom layout!"
---
# Page with Custom Layout
Content goes here...
Advanced Custom Layout
For more complex layouts, you can create a completely custom layout:
<!-- .vitepress/theme/CustomLayout.vue -->
<script setup>
import { useData, useRoute } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import { computed } from 'vue'
const { site, page, frontmatter } = useData()
const route = useRoute()
const showSidebar = computed(() => {
return frontmatter.value.sidebar !== false &&
route.path !== '/' &&
!frontmatter.value.home
})
</script>
<template>
<div class="custom-theme-layout">
<header class="custom-header">
<div class="container">
<h1>{{ site.title }}</h1>
<nav>
<a v-for="item in site.themeConfig.nav" :key="item.text" :href="item.link">
{{ item.text }}
</a>
</nav>
</div>
</header>
<div class="container main-container">
<aside v-if="showSidebar" class="sidebar">
<!-- Sidebar content -->
</aside>
<main class="content">
<div class="content-container">
<h1>{{ page.title }}</h1>
<Content />
</div>
</main>
</div>
<footer class="custom-footer">
<div class="container">
{{ site.themeConfig.footer?.copyright || `© ${new Date().getFullYear()} ${site.title}` }}
</div>
</footer>
</div>
</template>
<style scoped>
.custom-theme-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
.custom-header {
background-color: var(--vp-c-bg-soft);
padding: 16px 0;
border-bottom: 1px solid var(--vp-c-divider);
}
.custom-header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.custom-header h1 {
font-size: 1.5em;
margin: 0;
}
.custom-header nav {
display: flex;
gap: 16px;
}
.main-container {
display: flex;
flex: 1;
}
.sidebar {
width: 280px;
padding: 24px 0;
border-right: 1px solid var(--vp-c-divider);
}
.content {
flex: 1;
padding: 24px 0;
}
.content-container {
max-width: 800px;
}
.custom-footer {
background-color: var(--vp-c-bg-soft);
padding: 24px 0;
border-top: 1px solid var(--vp-c-divider);
text-align: center;
}
</style>
Use it in your theme entry file:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import CustomLayout from './CustomLayout.vue'
import './styles/custom.css'
export default {
...DefaultTheme,
Layout: CustomLayout
}
Customizing Theme Colors
Basic Color Customization
Customize theme colors using CSS variables:
/* .vitepress/theme/styles/custom.css */
:root {
/* Brand colors */
--vp-c-brand: #3eaf7c;
--vp-c-brand-light: #4abf8a;
--vp-c-brand-lighter: #5ecf99;
--vp-c-brand-dark: #369f6e;
--vp-c-brand-darker: #2e8f60;
/* Text colors */
--vp-c-text-1: rgba(60, 60, 60, 1);
--vp-c-text-2: rgba(60, 60, 60, 0.78);
--vp-c-text-3: rgba(60, 60, 60, 0.56);
/* Background colors */
--vp-c-bg: #ffffff;
--vp-c-bg-soft: #f9f9f9;
--vp-c-bg-mute: #f1f1f1;
/* Border colors */
--vp-c-border: #e2e2e2;
--vp-c-divider: #e2e2e2;
--vp-c-gutter: #e2e2e2;
}
.dark {
/* Dark theme colors */
--vp-c-text-1: rgba(255, 255, 255, 0.87);
--vp-c-text-2: rgba(255, 255, 255, 0.6);
--vp-c-text-3: rgba(255, 255, 255, 0.38);
--vp-c-bg: #1a1a1a;
--vp-c-bg-soft: #242424;
--vp-c-bg-mute: #2c2c2c;
--vp-c-border: #383838;
--vp-c-divider: #383838;
--vp-c-gutter: #383838;
}
Advanced Color Customization
Create a color scheme with CSS variables:
/* .vitepress/theme/styles/custom.css */
:root {
/* Primary color palette */
--color-primary-50: #f0f9ff;
--color-primary-100: #e0f2fe;
--color-primary-200: #bae6fd;
--color-primary-300: #7dd3fc;
--color-primary-400: #38bdf8;
--color-primary-500: #0ea5e9;
--color-primary-600: #0284c7;
--color-primary-700: #0369a1;
--color-primary-800: #075985;
--color-primary-900: #0c4a6e;
/* Secondary color palette */
--color-secondary-50: #f5f3ff;
--color-secondary-100: #ede9fe;
--color-secondary-200: #ddd6fe;
--color-secondary-300: #c4b5fd;
--color-secondary-400: #a78bfa;
--color-secondary-500: #8b5cf6;
--color-secondary-600: #7c3aed;
--color-secondary-700: #6d28d9;
--color-secondary-800: #5b21b6;
--color-secondary-900: #4c1d95;
/* Map to VitePress variables */
--vp-c-brand: var(--color-primary-600);
--vp-c-brand-light: var(--color-primary-500);
--vp-c-brand-lighter: var(--color-primary-400);
--vp-c-brand-dark: var(--color-primary-700);
--vp-c-brand-darker: var(--color-primary-800);
}
Custom Theme Features
Custom Home Page
Create a custom home page component:
<!-- .vitepress/theme/components/HomePage.vue -->
<script setup>
import { useData } from 'vitepress'
const { frontmatter } = useData()
</script>
<template>
<div class="home-page">
<div class="hero">
<h1>{{ frontmatter.heroTitle }}</h1>
<p>{{ frontmatter.heroTagline }}</p>
<div class="hero-actions">
<a class="button primary" :href="frontmatter.actionLink">
{{ frontmatter.actionText }}
</a>
<a class="button secondary" :href="frontmatter.altActionLink">
{{ frontmatter.altActionText }}
</a>
</div>
</div>
<div class="features">
<div v-for="(feature, index) in frontmatter.features" :key="index" class="feature">
<div class="feature-icon" v-html="feature.icon"></div>
<h2>{{ feature.title }}</h2>
<p>{{ feature.details }}</p>
</div>
</div>
</div>
</template>
<style scoped>
.home-page {
padding: 64px 24px;
max-width: 1200px;
margin: 0 auto;
}
.hero {
text-align: center;
margin-bottom: 64px;
}
.hero h1 {
font-size: 3em;
margin-bottom: 16px;
}
.hero p {
font-size: 1.5em;
color: var(--vp-c-text-2);
margin-bottom: 32px;
}
.hero-actions {
display: flex;
justify-content: center;
gap: 16px;
}
.button {
display: inline-block;
padding: 12px 24px;
border-radius: 8px;
font-weight: 500;
transition: background-color 0.2s;
text-decoration: none;
}
.button.primary {
background-color: var(--vp-c-brand);
color: white;
}
.button.primary:hover {
background-color: var(--vp-c-brand-dark);
}
.button.secondary {
background-color: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.button.secondary:hover {
background-color: var(--vp-c-bg-mute);
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 32px;
margin-top: 64px;
}
.feature {
padding: 24px;
border-radius: 8px;
background-color: var(--vp-c-bg-soft);
transition: transform 0.2s, box-shadow 0.2s;
}
.feature:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.feature-icon {
font-size: 2em;
margin-bottom: 16px;
color: var(--vp-c-brand);
}
.feature h2 {
margin-bottom: 12px;
}
.feature p {
color: var(--vp-c-text-2);
}
@media (max-width: 768px) {
.hero h1 {
font-size: 2.5em;
}
.hero p {
font-size: 1.2em;
}
.features {
grid-template-columns: 1fr;
}
}
</style>
Use it in your theme entry file:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import HomePage from './components/HomePage.vue'
import './styles/custom.css'
export default {
...DefaultTheme,
enhanceApp({ app }) {
app.component('HomePage', HomePage)
}
}
Create a home page with frontmatter:
---
layout: home
heroTitle: My Documentation Site
heroTagline: A beautiful documentation site powered by VitePress
actionText: Get Started
actionLink: /guide/
altActionText: Learn More
altActionLink: /about/
features:
- title: Feature One
details: Description of the first feature
icon: 📚
- title: Feature Two
details: Description of the second feature
icon: 🚀
- title: Feature Three
details: Description of the third feature
icon: 🔧
---
<HomePage />
Custom Sidebar
Create a custom sidebar component:
<!-- .vitepress/theme/components/CustomSidebar.vue -->
<script setup>
import { useData, useRoute } from 'vitepress'
import { computed } from 'vue'
const { theme } = useData()
const route = useRoute()
const sidebar = computed(() => {
return theme.value.sidebar
})
const currentPath = computed(() => route.path)
function isActive(link) {
return currentPath.value === link
}
</script>
<template>
<div class="custom-sidebar">
<div v-for="(section, index) in sidebar" :key="index" class="sidebar-section">
<h3 class="sidebar-section-title">{{ section.text }}</h3>
<ul class="sidebar-links">
<li v-for="item in section.items" :key="item.link" class="sidebar-link">
<a :href="item.link" :class="{ active: isActive(item.link) }">
{{ item.text }}
</a>
</li>
</ul>
</div>
</div>
</template>
<style scoped>
.custom-sidebar {
padding: 16px 0;
}
.sidebar-section {
margin-bottom: 24px;
}
.sidebar-section-title {
font-size: 0.9em;
font-weight: 600;
color: var(--vp-c-text-2);
text-transform: uppercase;
padding: 0 24px;
margin-bottom: 8px;
}
.sidebar-links {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-link a {
display: block;
padding: 8px 24px;
color: var(--vp-c-text-1);
text-decoration: none;
font-size: 0.95em;
transition: color 0.2s, background-color 0.2s;
border-left: 2px solid transparent;
}
.sidebar-link a:hover {
color: var(--vp-c-brand);
background-color: var(--vp-c-bg-soft);
}
.sidebar-link a.active {
color: var(--vp-c-brand);
background-color: var(--vp-c-bg-soft);
border-left-color: var(--vp-c-brand);
font-weight: 500;
}
</style>
Use it in your custom layout:
<!-- .vitepress/theme/Layout.vue -->
<template>
<div class="theme-container">
<!-- Header -->
<header>...</header>
<div class="theme-content">
<CustomSidebar v-if="showSidebar" />
<main>
<Content />
</main>
</div>
<!-- Footer -->
<footer>...</footer>
</div>
</template>
Custom Navigation
Create a custom navigation component:
<!-- .vitepress/theme/components/CustomNav.vue -->
<script setup>
import { useData } from 'vitepress'
import { ref } from 'vue'
const { site, theme } = useData()
const isMenuOpen = ref(false)
function toggleMenu() {
isMenuOpen.value = !isMenuOpen.value
}
</script>
<template>
<nav class="custom-nav">
<div class="nav-container">
<div class="nav-logo">
<a href="/">{{ site.title }}</a>
</div>
<div class="nav-links-desktop">
<a v-for="item in theme.nav" :key="item.text" :href="item.link" class="nav-link">
{{ item.text }}
</a>
</div>
<button class="menu-toggle" @click="toggleMenu">
<span class="menu-icon"></span>
</button>
</div>
<div class="nav-links-mobile" :class="{ open: isMenuOpen }">
<a v-for="item in theme.nav" :key="item.text" :href="item.link" class="nav-link" @click="isMenuOpen = false">
{{ item.text }}
</a>
</div>
</nav>
</template>
<style scoped>
.custom-nav {
background-color: var(--vp-c-bg);
border-bottom: 1px solid var(--vp-c-divider);
position: sticky;
top: 0;
z-index: 100;
}
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
padding: 16px 24px;
}
.nav-logo a {
font-size: 1.2em;
font-weight: 600;
color: var(--vp-c-text-1);
text-decoration: none;
}
.nav-links-desktop {
display: flex;
gap: 24px;
}
.nav-link {
color: var(--vp-c-text-1);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.nav-link:hover {
color: var(--vp-c-brand);
}
.menu-toggle {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 8px;
}
.menu-icon {
display: block;
width: 24px;
height: 2px;
background-color: var(--vp-c-text-1);
position: relative;
}
.menu-icon::before,
.menu-icon::after {
content: '';
position: absolute;
width: 24px;
height: 2px;
background-color: var(--vp-c-text-1);
transition: transform 0.2s;
}
.menu-icon::before {
top: -8px;
}
.menu-icon::after {
bottom: -8px;
}
.nav-links-mobile {
display: none;
}
@media (max-width: 768px) {
.nav-links-desktop {
display: none;
}
.menu-toggle {
display: block;
}
.nav-links-mobile {
display: flex;
flex-direction: column;
padding: 0;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s, padding 0.3s;
}
.nav-links-mobile.open {
max-height: 300px;
padding: 16px 24px;
border-top: 1px solid var(--vp-c-divider);
}
.nav-links-mobile .nav-link {
padding: 12px 0;
}
}
</style>
Advanced Theme Customization
Dark Mode Toggle
Create a dark mode toggle component:
<!-- .vitepress/theme/components/DarkModeToggle.vue -->
<script setup>
import { useData } from 'vitepress'
import { ref, watch, onMounted } from 'vue'
const { isDark } = useData()
const darkMode = ref(false)
watch(isDark, (newVal) => {
darkMode.value = newVal
})
function toggleDarkMode() {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
localStorage.setItem('vitepress-theme-appearance', isDark.value ? 'dark' : 'light')
}
onMounted(() => {
darkMode.value = isDark.value
})
</script>
<template>
<button class="dark-mode-toggle" @click="toggleDarkMode" :title="darkMode ? 'Switch to light mode' : 'Switch to dark mode'">
<svg v-if="darkMode" class="sun-icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM11 1H13V4H11V1ZM11 20H13V23H11V20ZM3.51472 4.92893L4.92893 3.51472L7.05025 5.63604L5.63604 7.05025L3.51472 4.92893ZM16.9497 18.364L18.364 16.9497L20.4853 19.0711L19.0711 20.4853L16.9497 18.364ZM19.0711 3.51472L20.4853 4.92893L18.364 7.05025L16.9497 5.63604L19.0711 3.51472ZM5.63604 16.9497L7.05025 18.364L4.92893 20.4853L3.51472 19.0711L5.63604 16.9497ZM23 11V13H20V11H23ZM4 11V13H1V11H4Z"></path>
</svg>
<svg v-else class="moon-icon" viewBox="0 0 24 24">
<path fill="currentColor" d="M10 7C10 10.866 13.134 14 17 14C18.9584 14 20.729 13.1957 21.9995 11.8995C22 11.933 22 11.9665 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C12.0335 2 12.067 2 12.1005 2.00049C10.8043 3.27098 10 5.04157 10 7ZM4 12C4 16.4183 7.58172 20 12 20C15.0583 20 17.7158 18.2839 19.062 15.7621C18.3945 15.9187 17.7035 16 17 16C12.0294 16 8 11.9706 8 7C8 6.29648 8.08133 5.60547 8.2379 4.938C5.71611 6.28423 4 8.9417 4 12Z"></path>
</svg>
</button>
</template>
<style scoped>
.dark-mode-toggle {
background: none;
border: none;
cursor: pointer;
padding: 8px;
color: var(--vp-c-text-1);
transition: color 0.2s;
}
.dark-mode-toggle:hover {
color: var(--vp-c-brand);
}
.sun-icon, .moon-icon {
width: 24px;
height: 24px;
}
</style>
Frequently Asked Questions
How do I add custom fonts to my theme?
Add custom fonts using CSS:
/* .vitepress/theme/styles/fonts.css */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/CustomFont.woff2') format('woff2'),
url('/fonts/CustomFont.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
:root {
--vp-font-family-base: 'CustomFont', system-ui, sans-serif;
--vp-font-family-mono: 'JetBrains Mono', monospace;
}
How do I add analytics to my VitePress site?
Add Google Analytics or other tracking tools by creating a plugin:
// .vitepress/theme/plugins/analytics.js
export default {
name: 'analytics',
enhanceApp({ router }) {
// Google Analytics example
if (process.env.NODE_ENV === 'production' && typeof window !== 'undefined') {
// Load Google Analytics script
const script = document.createElement('script')
script.async = true
script.src = 'https://www.googletagmanager.com/gtag/js?id=YOUR_GA_ID'
document.head.appendChild(script)
window.dataLayer = window.dataLayer || []
function gtag() {
dataLayer.push(arguments)
}
gtag('js', new Date())
gtag('config', 'YOUR_GA_ID')
// Track page views
router.onAfterRouteChanged = (to) => {
gtag('event', 'page_view', {
page_path: to
})
}
}
}
}
Use it in your theme:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './styles/custom.css'
import analyticsPlugin from './plugins/analytics'
export default {
...DefaultTheme,
enhanceApp(ctx) {
DefaultTheme.enhanceApp(ctx)
analyticsPlugin.enhanceApp(ctx)
}
}
How do I create a responsive theme?
Use CSS media queries for responsive design:
/* .vitepress/theme/styles/responsive.css */
/* Base styles for all screen sizes */
.container {
padding: 0 24px;
margin: 0 auto;
}
/* Small screens (mobile) */
@media (max-width: 640px) {
.container {
padding: 0 16px;
}
.sidebar {
display: none;
}
.mobile-nav {
display: block;
}
}
/* Medium screens (tablets) */
@media (min-width: 641px) and (max-width: 1024px) {
.container {
max-width: 768px;
}
.sidebar {
width: 220px;
}
}
/* Large screens (desktops) */
@media (min-width: 1025px) {
.container {
max-width: 1200px;
}
.sidebar {
width: 280px;
}
.mobile-nav {
display: none;
}
}
How do I add custom code highlighting themes?
Create a custom code highlighting theme:
/* .vitepress/theme/styles/code.css */
:root {
--code-bg-color: #f8f8f8;
--code-text-color: #333;
--code-keyword-color: #d73a49;
--code-function-color: #6f42c1;
--code-string-color: #032f62;
--code-comment-color: #6a737d;
--code-number-color: #005cc5;
}
.dark {
--code-bg-color: #1e1e1e;
--code-text-color: #d4d4d4;
--code-keyword-color: #c586c0;
--code-function-color: #dcdcaa;
--code-string-color: #ce9178;
--code-comment-color: #6a9955;
--code-number-color: #b5cea8;
}
div[class*='language-'] {
background-color: var(--code-bg-color);
}
div[class*='language-'] code {
color: var(--code-text-color);
}
.token.keyword {
color: var(--code-keyword-color);
}
.token.function {
color: var(--code-function-color);
}
.token.string {
color: var(--code-string-color);
}
.token.comment {
color: var(--code-comment-color);
}
.token.number {
color: var(--code-number-color);
}
Import it in your theme:
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import './styles/code.css'
import './styles/custom.css'
export default {
...DefaultTheme
}
Resources and References
- VitePress Documentation
- Vue.js Documentation
- Markdown Guide
- CSS Variables Guide
- Vue Component Best Practices