skip to content

How do you extend the VitePress 1.6 default theme to register demo components globally, apply a brand colour and add a release banner to every page, without forking it?

level: seniorimportance: should knowfreq 18%

answer

  1. a theme entry in .vitepress/theme
  2. extends keeps the parent's setup
  3. CSS variables, not config
  4. named slots on the layout
  5. internal components can be aliased

basics

~10 s

Create .vitepress/theme/index.ts that exports { extends: DefaultTheme, enhanceApp, Layout }: register components in enhanceApp, override --vp-c-brand-1 in an imported CSS file, and fill the default Layout's layout-top slot through a wrapper.

solid answer

~40 s

A VitePress theme is the default export of `.vitepress/theme/index.ts`: an object with a required `Layout`, an optional `enhanceApp({ app, router, siteData })` and an optional `extends`. To customise the default theme without forking it, export `{ extends: DefaultTheme, Layout, enhanceApp }`. `extends` runs the default theme's own `enhanceApp` before yours, so its built-in pieces keep working. In `enhanceApp`, call `app.component('ButtonBasic', ButtonBasic)` for demos used everywhere. Branding is CSS: import a stylesheet that overrides variables such as `--vp-c-brand-1` on `:root`. For the banner, wrap `DefaultTheme.Layout` in your own component and fill a layout slot: `layout-top` renders on every page, while `doc-before` renders only with the `doc` layout. Replacing an internal component such as `VPNavBar.vue` is possible with a Vite alias, but internal names can change between minors, so treat it as a last resort.

code

vue · 13 lines
vue
<!-- .vitepress/theme/ReleaseLayout.vue -->
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
const { Layout } = DefaultTheme
</script>

<template>
  <Layout>
    <template #layout-top>
      <div class="release-banner">Nimbus UI 3.0 is out: read the migration guide.</div>
    </template>
  </Layout>
</template>

go deeper

for a junior

Recall that the theme lives in .vitepress/theme/index, that colours are CSS variables, and that enhanceApp can register global components.

for a middle

Explain the Theme contract, what extends does, and how layout slots let you inject content without replacing the layout.

for a senior

Show how you pick slots by layout, avoid dropping the default enhanceApp, and weigh aliasing internal components against upgrade risk.

for a principal

Decide how far the docs brand diverges from the default theme, balancing identity against the cost of tracking VitePress releases.

## What a VitePress theme is The default export of `.vitepress/theme/index.ts` (or `.js`) is the theme. Its contract is small: | Property | Required | Purpose | |---|---|---| | `Layout` | yes | the root component rendered for every page | | `enhanceApp(ctx)` | no | runs with `{ app, router, siteData }` to register components, plugins or router hooks; may be async | | `extends` | no | another theme whose `enhanceApp` runs before this one | With no theme directory, VitePress uses the default theme. Creating one lets you **extend** the default theme rather than replace it. ## Extending instead of forking ```ts // .vitepress/theme/index.ts import type { Theme } from 'vitepress' import DefaultTheme from 'vitepress/theme' import ReleaseLayout from './ReleaseLayout.vue' import ButtonBasic from '../../demos/ButtonBasic.vue' import './brand.css' export default { extends: DefaultTheme, Layout: ReleaseLayout, enhanceApp({ app }) { app.component('ButtonBasic', ButtonBasic) }, } satisfies Theme ``` Three customisation levels are in play: 1. **CSS variables.** The default theme's colours, fonts and spacing are CSS custom properties on `:root`. Overriding `--vp-c-brand-1` and `--vp-c-brand-2` in an imported stylesheet rebrands buttons, links and accents; dark mode is a `.dark` class on `<html>`, so a `.dark` rule can set darker-mode values. ```css /* .vitepress/theme/brand.css */ :root { --vp-c-brand-1: #0e7c86; --vp-c-brand-2: #12939f; } .dark { --vp-c-brand-1: #4fc3cc; } ``` To drop the bundled Inter font, import the theme from `vitepress/theme-without-fonts` and set `--vp-font-family-base`. 2. **`enhanceApp`.** It receives the Vue app, so `app.component(...)` registers global components and `app.use(...)` installs Vue plugins. Because it can be async, a plugin that touches the browser on import can be loaded with a dynamic import guarded by `import.meta.env.SSR`. 3. **Layout slots.** The default `Layout` exposes named slots. A wrapper component renders `<DefaultTheme.Layout>` and fills the slots it needs. ## Choosing a slot | Slot | Rendered | |---|---| | `layout-top`, `layout-bottom` | on every page | | `nav-bar-title-after`, `nav-bar-content-before` | in the nav bar, every page | | `doc-before`, `doc-after`, `doc-footer-before` | only with `layout: doc`, the default for Markdown pages | | `aside-outline-before`, `aside-ads-after` | in the right-hand aside of doc pages | | `home-hero-after`, `home-features-before` | only with `layout: home` | | `page-top`, `page-bottom` | only with `layout: page` | | `not-found` | on the 404 page | A release banner meant for every page belongs in `layout-top`. Put it in `doc-before` and the home page and any `layout: page` page will not show it. The wrapper can also be a render function instead of a `.vue` file: `Layout() { return h(DefaultTheme.Layout, null, { 'layout-top': () => h(ReleaseBanner) }) }` inside the theme object. ## Replacing internal components When slots are not enough, a Vite alias can swap an internal default-theme component, for example matching `/^.*\/VPNavBar\.vue$/` and pointing it at your own file through the `vite.resolve.alias` option in the site config. The docs warn that internal component names can change between minor releases, so every VitePress upgrade needs a check. ## When to write a custom theme instead If the site needs a different structure altogether, a theme with only a `Layout` component that renders `<Content />` (the page's compiled Markdown) is enough. You then own navigation, search and dark mode yourself. For a component library's docs, extending the default theme is usually the better trade: you keep its features and upgrades. ## Order and gotchas - `extends` runs the parent theme's `enhanceApp` first, then yours. Exporting your own `enhanceApp` without `extends` silently drops the default theme's, which in 1.6 registers the global `Badge` component. - Replacing `Layout` without rendering `DefaultTheme.Layout` inside it drops the whole default UI. - Theme code is rendered during the build like pages, so it must be SSR-compatible. - A slot added to the wrapper affects every page, so keep banner components light; they run on every navigation. ## Deciding how far to go | Need | Lightest tool that works | |---|---| | brand colour, fonts, spacing | CSS variables | | global demo components, Vue plugins, router hooks | `enhanceApp` | | extra content in known places | layout slots through a wrapper `Layout` | | a different nav bar or footer | aliasing one internal component, with upgrade checks | | a different site structure | a custom theme with its own `Layout` and `<Content />` | Each step down the table costs more upkeep. A library's docs usually need only the first three: its identity comes from colour, a logo and the live demos, not from a new layout. Keeping to them means every VitePress minor release is an ordinary dependency bump rather than a merge of forked theme code.

  • What breaks if the theme exports { Layout: DefaultTheme.Layout, enhanceApp } without extends?
    The layout renders, but the default theme's own `enhanceApp` never runs, so what it registers is missing; in 1.6 that is the global `Badge` component pages use in Markdown. Spreading `...DefaultTheme` has the same problem once you define your own `enhanceApp`, because yours replaces it. `extends: DefaultTheme` chains them: parent first, then yours.
  • How would you register every demo component in a folder globally?
    In `enhanceApp`, use Vite's glob import, such as `import.meta.glob('../../demos/*.vue', { eager: true })`, then loop over the modules and call `app.component(name, module.default)` with a name derived from each file. It saves imports on every page, at the cost of shipping all demos with the theme.

saying these in an interview costs you the question

  • Customising the default theme requires copying its source into your project.
  • The brand colour is set with a themeConfig option in the site config.
  • The doc-before slot also renders on the home page.
  • A custom theme must implement enhanceApp; Layout is optional.
  • Aliasing internal components like VPNavBar.vue is a stable, versioned API.