Skip to content

Extending Default Theme

Customize the default theme through CSS, slots, and Vue components.

Theme Entry File

Create .vitepress/theme/index.ts to extend the default theme:

ts
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme';
import './custom.css';

export default DefaultTheme;

CSS Variables

Override root CSS variables:

css
/* .vitepress/theme/custom.css */
:root {
  /* Brand colors */
  --vp-c-brand-1: #646cff;
  --vp-c-brand-2: #747bff;
  --vp-c-brand-3: #9499ff;

  /* Backgrounds */
  --vp-c-bg: #ffffff;
  --vp-c-bg-soft: #f6f6f7;

  /* Text */
  --vp-c-text-1: #213547;
  --vp-c-text-2: #476582;
}

.dark {
  --vp-c-brand-1: #747bff;
  --vp-c-bg: #1a1a1a;
}

See all CSS variables.

Home Hero Customization

css
:root {
  /* Gradient name color */
  --vp-home-hero-name-color: transparent;
  --vp-home-hero-name-background: linear-gradient(120deg, #bd34fe, #41d1ff);

  /* Hero image glow */
  --vp-home-hero-image-background-image: linear-gradient(
    -45deg,
    #bd34fe 50%,
    #47caff 50%
  );
  --vp-home-hero-image-filter: blur(44px);
}

Custom Fonts

Remove Inter font and use your own:

ts
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme-without-fonts';
import './fonts.css';

export default DefaultTheme;
css
/* .vitepress/theme/fonts.css */
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/myfont.woff2') format('woff2');
}

:root {
  --vp-font-family-base: 'MyFont', sans-serif;
  --vp-font-family-mono: 'Fira Code', monospace;
}

Preload fonts in config:

ts
// .vitepress/config.ts
export default {
  transformHead({ assets }) {
    const fontFile = assets.find((file) => /myfont\.[\w-]+\.woff2/.test(file));
    if (fontFile) {
      return [
        [
          'link',
          {
            rel: 'preload',
            href: fontFile,
            as: 'font',
            type: 'font/woff2',
            crossorigin: '',
          },
        ],
      ];
    }
  },
};

Global Components

Register components available in all markdown:

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

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

Use in markdown:

md
<MyComponent :prop="value" />

Layout Slots

Inject content into specific locations:

ts
// .vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme';
import MyLayout from './MyLayout.vue';

export default {
  extends: DefaultTheme,
  Layout: MyLayout,
};
vue
<!-- .vitepress/theme/MyLayout.vue -->
<script setup>
import DefaultTheme from 'vitepress/theme';
const { Layout } = DefaultTheme;
</script>

<template>
  <Layout>
    <template #aside-outline-before>
      <div>Above outline</div>
    </template>

    <template #doc-before>
      <div>Before doc content</div>
    </template>

    <template #doc-after>
      <div>After doc content</div>
    </template>
  </Layout>
</template>

Available Slots

Doc layout (layout: doc):

  • doc-top, doc-bottom
  • doc-before, doc-after
  • doc-footer-before
  • sidebar-nav-before, sidebar-nav-after
  • aside-top, aside-bottom
  • aside-outline-before, aside-outline-after
  • aside-ads-before, aside-ads-after

Home layout (layout: home):

  • home-hero-before, home-hero-after
  • home-hero-info-before, home-hero-info, home-hero-info-after
  • home-hero-actions-after, home-hero-image
  • home-features-before, home-features-after

Page layout (layout: page):

  • page-top, page-bottom

Always available:

  • layout-top, layout-bottom
  • nav-bar-title-before, nav-bar-title-after
  • nav-bar-content-before, nav-bar-content-after
  • not-found (404 page)

Using Render Functions

Alternative to template slots:

ts
// .vitepress/theme/index.ts
import { h } from 'vue';
import DefaultTheme from 'vitepress/theme';
import MyComponent from './MyComponent.vue';

export default {
  extends: DefaultTheme,
  Layout() {
    return h(DefaultTheme.Layout, null, {
      'aside-outline-before': () => h(MyComponent),
    });
  },
};

Override Internal Components

Replace default theme components with Vite aliases:

ts
// .vitepress/config.ts
import { fileURLToPath, URL } from 'node:url';

export default {
  vite: {
    resolve: {
      alias: [
        {
          find: /^.*\/VPNavBar\.vue$/,
          replacement: fileURLToPath(
            new URL('./theme/components/CustomNavBar.vue', import.meta.url),
          ),
        },
      ],
    },
  },
};

View Transitions

Custom dark mode toggle animation:

vue
<!-- .vitepress/theme/Layout.vue -->
<script setup>
import { useData } from 'vitepress';
import DefaultTheme from 'vitepress/theme';
import { nextTick, provide } from 'vue';

const { isDark } = useData();

provide('toggle-appearance', async ({ clientX: x, clientY: y }) => {
  if (!document.startViewTransition) {
    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>

Key Points

  • Import vitepress/theme-without-fonts to use custom fonts
  • Use layout slots to inject content without overriding components
  • Global components are registered in enhanceApp
  • Override CSS variables for theming
  • Use Vite aliases to replace internal components

Released under the CC BY-SA 4.0 License. (134a8ec)