Files
stack/packages/mosaic/framework/skills/vitepress/references/theme-customization.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

6.1 KiB

name, description
name description
extending-vitepress-default-theme Customize CSS variables, use layout slots, register global components, and override theme fonts

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:

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

export default DefaultTheme;

CSS Variables

Override root CSS variables:

/* .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

: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:

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

export default DefaultTheme;
/* .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:

// .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:

// .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:

<MyComponent :prop="value" />

Layout Slots

Inject content into specific locations:

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

export default {
  extends: DefaultTheme,
  Layout: MyLayout,
};
<!-- .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:

// .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:

// .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:

<!-- .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