Skip to content

About this page

Edit this page on GitHub

Last updated:

On this page

Last updated:

Creating Components

This guide walks through building your own Vue components for a VitePress Carbon site — from a one-off component used on a single page to globally registered components available in every Markdown file.

Project structure

Custom theme code lives in the .vitepress/theme directory:

docs/
├─ .vitepress/
│  ├─ config.mts
│  └─ theme/
│     ├─ index.ts            ← theme entry
│     └─ components/
│        ├─ MyCard.vue
│        └─ RepoStars.vue
└─ index.md

If you don't have a .vitepress/theme/index.ts yet, create one that re-exports the Carbon theme:

ts
// .vitepress/theme/index.ts
import { VPCarbon } from 'vitepress-carbon'

export default {
  ...VPCarbon
}

Your first component

A component is a regular Vue single-file component. Style it with the theme's CSS variables so it automatically adapts to light and dark mode:

vue
<!-- .vitepress/theme/components/MyCard.vue -->
<script setup lang="ts">
defineProps<{
  title: string
}>()
</script>

<template>
  <div class="my-card">
    <h3>{{ title }}</h3>
    <div class="my-card-body">
      <slot />
    </div>
  </div>
</template>

<style scoped>
.my-card {
  border: 1px solid var(--vp-c-divider);
  border-radius: 8px;
  padding: 16px 20px;
  background-color: var(--vp-c-bg-soft);
}

.my-card h3 {
  margin: 0 0 8px;
  color: var(--vp-c-text-1);
}

.my-card-body {
  color: var(--vp-c-text-2);
  font-size: 14px;
}
</style>

Use the theme's CSS variables

Hard-coded colors break in one of the two color modes. Prefer the theme variables — the most useful ones:

VariablePurpose
--vp-c-bg / --vp-c-bg-alt / --vp-c-bg-softBackground surfaces
--vp-c-text-1 / --vp-c-text-2 / --vp-c-text-3Text, from strongest to softest
--vp-c-divider / --vp-c-borderBorders and separators
--vp-c-brand-1--vp-c-brand-3Brand / accent colors
--vp-c-tip-1, --vp-c-warning-1, --vp-c-danger-1Status colors

The full list is in the theme's vars.css.

Using a component

On a single page

Import it directly in the Markdown file:

md
<script setup>
import MyCard from '../.vitepress/theme/components/MyCard.vue'
</script>

# A Page

<MyCard title="Hello">
  Any **Markdown** or HTML works inside the slot.
</MyCard>

Globally, on every page

Register it once in enhanceApp:

ts
// .vitepress/theme/index.ts
import { VPCarbon } from 'vitepress-carbon'
import MyCard from './components/MyCard.vue'

export default {
  ...VPCarbon,
  enhanceApp({ app }) {
    app.component('MyCard', MyCard)
  }
}

Now <MyCard> works in any Markdown file with no import.

Inside the theme's layout slots

Components can also be injected into fixed positions of the layout (navbar, sidebar, before/after content, …) using layout slots — see Extending Theme and the full slot reference.

Accessing site and page data

Use VitePress's useData inside components to read the site config, frontmatter, and current page:

vue
<script setup lang="ts">
import { useData } from 'vitepress'

const { site, page, frontmatter, isDark } = useData()
</script>

<template>
  <p>
    Reading <code>{{ page.relativePath }}</code> from {{ site.title }}
  </p>
  <p v-if="frontmatter.author">Written by {{ frontmatter.author }}</p>
</template>

Carbon additionally exports useSidebar and useLocalNav:

ts
import { useSidebar, useLocalNav } from 'vitepress-carbon'

Building on the theme's components

Your components can compose the built-in components:

vue
<!-- .vitepress/theme/components/ProjectLinks.vue -->
<script setup lang="ts">
import { VPButton, VPSocialLinks } from 'vitepress-carbon/components'

const links = [
  { icon: 'github', link: 'https://github.com/brenoepics/vitepress-carbon' }
]
</script>

<template>
  <div class="project-links">
    <VPButton text="Get Started" href="/guide/getting-started" />
    <VPSocialLinks :links="links" />
  </div>
</template>

SSR: browser-only code

VitePress pre-renders every page in Node, where window and document don't exist. If a component (or a library it imports) touches browser APIs on load, guard it:

  • onMounted — runs only in the browser:

    vue
    <script setup>
    import { onMounted, ref } from 'vue'
    
    const width = ref(0)
    onMounted(() => {
      width.value = window.innerWidth
    })
    </script>
  • <ClientOnly> — skips SSR entirely for non-SSR-friendly components:

    md
    <ClientOnly>
      <MyBrowserOnlyChart />
    </ClientOnly>
  • Dynamic import — for libraries that break just by being imported:

    ts
    onMounted(async () => {
      const { default: lib } = await import('some-browser-lib')
      lib.init()
    })

Fetching data at build time

For data that should be baked into the site (instead of fetched in the browser), use VitePress build-time data loaders. Create a *.data.ts file and import it from any component:

ts
// .vitepress/theme/data/stars.data.ts
export default {
  async load() {
    const res = await fetch(
      'https://api.github.com/repos/brenoepics/vitepress-carbon'
    )
    const { stargazers_count } = await res.json()
    return { stars: stargazers_count }
  }
}
vue
<script setup lang="ts">
import { data } from '../data/stars.data.ts'
</script>

<template>
  <span>⭐ {{ data.stars }}</span>
</template>

The loader runs once at build time; the browser never calls the GitHub API.

Checklist

  • Component lives in .vitepress/theme/components/
  • Colors use --vp-* CSS variables (works in light and dark mode)
  • Browser APIs only inside onMounted (or wrapped in <ClientOnly>)
  • Registered in enhanceApp if used on more than one page

Next steps