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?
answer
- a theme entry in .vitepress/theme
- extends keeps the parent's setup
- CSS variables, not config
- named slots on the layout
- internal components can be aliased
basics
~10 sCreate .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 sA 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<!-- .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
Recall that the theme lives in .vitepress/theme/index, that colours are CSS variables, and that enhanceApp can register global components.
Explain the Theme contract, what extends does, and how layout slots let you inject content without replacing the layout.
Show how you pick slots by layout, avoid dropping the default enhanceApp, and weigh aliasing internal components against upgrade risk.
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.