Skip to content

Using Vue in Markdown ​

Learn how to use Vue components and features directly in your Markdown files.

Vue Components in Markdown ​

Basic Component Usage ​

You can use Vue components directly in Markdown:

markdown
<script setup>
import CustomButton from './components/CustomButton.vue'
</script>

# My Page

Here's a custom button component:

<CustomButton text="Click me!" @click="handleClick" />

Regular markdown content continues here.

Inline Components ​

Define components inline:

markdown
<script setup>
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++
</script>

# Counter Example

<div class="counter">
  <p>Count: {{ count }}</p>
  <button @click="increment">Increment</button>
</div>

<style scoped>
.counter {
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 8px;
  margin: 20px 0;
}

button {
  background: #42b883;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}
</style>

Global Components ​

Register components globally in your theme:

javascript
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import CustomButton from './components/CustomButton.vue'
import InfoBox from './components/InfoBox.vue'

export default {
  extends: DefaultTheme,
  enhanceApp({ app }) {
    app.component('CustomButton', CustomButton)
    app.component('InfoBox', InfoBox)
  }
}

Then use them anywhere in Markdown:

markdown
# My Page

<InfoBox type="warning">
This is a warning message using a global component.
</InfoBox>

<CustomButton>Global Button</CustomButton>

Vue Expressions ​

Template Expressions ​

Use Vue template expressions in Markdown:

markdown
# Dynamic Content

Current year: {{ new Date().getFullYear() }}

Math calculation: {{ 2 + 3 * 4 }}

Conditional text: {{ user ? `Hello, ${user.name}!` : 'Please log in' }}

Reactive Data ​

Use reactive data in expressions:

markdown
<script setup>
import { ref, computed } from 'vue'

const name = ref('World')
const greeting = computed(() => `Hello, ${name.value}!`)
</script>

# Interactive Greeting

<input v-model="name" placeholder="Enter your name" />

{{ greeting }}

Vue Directives ​

v-if and v-show ​

Conditional rendering:

markdown
<script setup>
import { ref } from 'vue'

const showDetails = ref(false)
const isLoggedIn = ref(false)
</script>

# Conditional Content

<button @click="showDetails = !showDetails">
  {{ showDetails ? 'Hide' : 'Show' }} Details
</button>

<div v-if="showDetails" class="details">
  <p>Here are the details!</p>
</div>

<div v-show="isLoggedIn">
  Welcome back, user!
</div>

<div v-if="!isLoggedIn">
  Please log in to continue.
</div>

v-for ​

List rendering:

markdown
<script setup>
const items = [
  { id: 1, name: 'Apple', price: 1.20 },
  { id: 2, name: 'Banana', price: 0.80 },
  { id: 3, name: 'Orange', price: 1.50 }
]

const users = ['Alice', 'Bob', 'Charlie']
</script>

# Dynamic Lists

## Fruit Prices

<ul>
  <li v-for="item in items" :key="item.id">
    {{ item.name }} - ${{ item.price }}
  </li>
</ul>

## User List

<ol>
  <li v-for="(user, index) in users" :key="index">
    {{ index + 1 }}. {{ user }}
  </li>
</ol>

v-model ​

Two-way data binding:

markdown
<script setup>
import { ref } from 'vue'

const message = ref('Hello VitePress!')
const selected = ref('option1')
const checked = ref(false)
const multiSelect = ref([])
</script>

# Form Controls

## Text Input
<input v-model="message" />
<p>Message: {{ message }}</p>

## Select
<select v-model="selected">
  <option value="option1">Option 1</option>
  <option value="option2">Option 2</option>
  <option value="option3">Option 3</option>
</select>
<p>Selected: {{ selected }}</p>

## Checkbox
<label>
  <input type="checkbox" v-model="checked" />
  Check me
</label>
<p>Checked: {{ checked }}</p>

## Multiple Checkboxes
<label>
  <input type="checkbox" value="vue" v-model="multiSelect" />
  Vue
</label>
<label>
  <input type="checkbox" value="react" v-model="multiSelect" />
  React
</label>
<label>
  <input type="checkbox" value="angular" v-model="multiSelect" />
  Angular
</label>
<p>Selected: {{ multiSelect.join(', ') }}</p>

Event Handling ​

Click Events ​

Handle user interactions:

markdown
<script setup>
import { ref } from 'vue'

const count = ref(0)
const message = ref('')

function increment() {
  count.value++
}

function showAlert() {
  alert('Button clicked!')
}

function handleInput(event) {
  message.value = event.target.value.toUpperCase()
}
</script>

# Event Handling

<button @click="increment">
  Clicked {{ count }} times
</button>

<button @click="showAlert">
  Show Alert
</button>

<input @input="handleInput" placeholder="Type something" />
<p>Uppercase: {{ message }}</p>

Custom Events ​

Handle custom component events:

markdown
<script setup>
import { ref } from 'vue'
import CustomInput from './components/CustomInput.vue'

const inputValue = ref('')

function handleCustomEvent(value) {
  inputValue.value = value
  console.log('Custom event received:', value)
}
</script>

# Custom Events

<CustomInput @custom-change="handleCustomEvent" />

<p>Value from custom component: {{ inputValue }}</p>

Lifecycle Hooks ​

Component Lifecycle ​

Use Vue lifecycle hooks:

markdown
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const time = ref(new Date().toLocaleTimeString())
let timer = null

onMounted(() => {
  console.log('Component mounted')
  
  timer = setInterval(() => {
    time.value = new Date().toLocaleTimeString()
  }, 1000)
})

onUnmounted(() => {
  console.log('Component unmounted')
  
  if (timer) {
    clearInterval(timer)
  }
})
</script>

# Live Clock

Current time: {{ time }}

Watchers ​

Watch for data changes:

markdown
<script setup>
import { ref, watch } from 'vue'

const searchTerm = ref('')
const searchResults = ref([])

watch(searchTerm, async (newTerm) => {
  if (newTerm.length > 2) {
    // Simulate API call
    searchResults.value = [
      `Result 1 for "${newTerm}"`,
      `Result 2 for "${newTerm}"`,
      `Result 3 for "${newTerm}"`
    ]
  } else {
    searchResults.value = []
  }
})
</script>

# Search Example

<input v-model="searchTerm" placeholder="Search..." />

<ul v-if="searchResults.length > 0">
  <li v-for="result in searchResults" :key="result">
    {{ result }}
  </li>
</ul>

<p v-else-if="searchTerm.length > 0 && searchTerm.length <= 2">
  Type at least 3 characters to search
</p>

Computed Properties ​

Basic Computed ​

Create computed properties:

markdown
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

const reversedName = computed(() => {
  return fullName.value.split('').reverse().join('')
})
</script>

# Computed Properties

<input v-model="firstName" placeholder="First name" />
<input v-model="lastName" placeholder="Last name" />

<p>Full name: {{ fullName }}</p>
<p>Reversed: {{ reversedName }}</p>

Complex Computed ​

More complex computed logic:

markdown
<script setup>
import { ref, computed } from 'vue'

const items = ref([
  { name: 'Apple', price: 1.20, category: 'fruit' },
  { name: 'Carrot', price: 0.80, category: 'vegetable' },
  { name: 'Banana', price: 0.90, category: 'fruit' },
  { name: 'Broccoli', price: 2.00, category: 'vegetable' }
])

const selectedCategory = ref('all')

const filteredItems = computed(() => {
  if (selectedCategory.value === 'all') {
    return items.value
  }
  return items.value.filter(item => item.category === selectedCategory.value)
})

const totalPrice = computed(() => {
  return filteredItems.value.reduce((sum, item) => sum + item.price, 0).toFixed(2)
})
</script>

# Shopping List

<select v-model="selectedCategory">
  <option value="all">All Categories</option>
  <option value="fruit">Fruits</option>
  <option value="vegetable">Vegetables</option>
</select>

<ul>
  <li v-for="item in filteredItems" :key="item.name">
    {{ item.name }} - ${{ item.price }} ({{ item.category }})
  </li>
</ul>

<p><strong>Total: ${{ totalPrice }}</strong></p>

Styling ​

Scoped Styles ​

Add component-specific styles:

markdown
<script setup>
import { ref } from 'vue'

const isActive = ref(false)
</script>

# Styled Component

<div class="card" :class="{ active: isActive }">
  <h3>Card Title</h3>
  <p>This is a styled card component.</p>
  <button @click="isActive = !isActive">
    {{ isActive ? 'Deactivate' : 'Activate' }}
  </button>
</div>

<style scoped>
.card {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 20px;
  margin: 20px 0;
  transition: all 0.3s ease;
}

.card.active {
  border-color: #42b883;
  box-shadow: 0 4px 12px rgba(66, 184, 131, 0.15);
}

.card h3 {
  margin-top: 0;
  color: #2c3e50;
}

.card button {
  background: #42b883;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  transition: background 0.2s;
}

.card button:hover {
  background: #369870;
}
</style>

CSS Modules ​

Use CSS modules for styling:

markdown
<script setup>
import { ref } from 'vue'
import styles from './styles.module.css'

const count = ref(0)
</script>

# CSS Modules Example

<div :class="styles.container">
  <button :class="styles.button" @click="count++">
    Count: {{ count }}
  </button>
</div>

Advanced Patterns ​

Composables ​

Create and use composables:

markdown
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

// Composable for mouse position
function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

// Composable for local storage
function useLocalStorage(key, defaultValue) {
  const storedValue = localStorage.getItem(key)
  const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)

  function setValue(newValue) {
    value.value = newValue
    localStorage.setItem(key, JSON.stringify(newValue))
  }

  return { value, setValue }
}

// Use composables
const { x, y } = useMouse()
const { value: savedText, setValue: setSavedText } = useLocalStorage('savedText', '')
</script>

# Composables Example

## Mouse Position
Mouse position: {{ x }}, {{ y }}

## Local Storage
<input :value="savedText" @input="setSavedText($event.target.value)" />
<p>Saved text: {{ savedText }}</p>

Provide/Inject ​

Share data across components:

markdown
<script setup>
import { provide, inject, ref } from 'vue'

// Provider component
const theme = ref('light')
provide('theme', theme)

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>

# Theme Provider

<div :class="`theme-${theme}`">
  <button @click="toggleTheme">
    Switch to {{ theme === 'light' ? 'dark' : 'light' }} theme
  </button>
  
  <ChildComponent />
</div>

<style scoped>
.theme-light {
  background: white;
  color: black;
}

.theme-dark {
  background: #1a1a1a;
  color: white;
}
</style>

Best Practices ​

Performance ​

  • Use v-show instead of v-if for frequently toggled elements
  • Implement proper key attributes for v-for
  • Use computed properties for expensive calculations
  • Lazy load components when possible

Accessibility ​

  • Add proper ARIA attributes
  • Ensure keyboard navigation works
  • Use semantic HTML elements
  • Provide alternative text for images

Code Organization ​

  • Keep components small and focused
  • Use composables for reusable logic
  • Separate concerns properly
  • Add TypeScript for better development experience

SEO Considerations ​

  • Be aware that dynamic content may not be indexed
  • Use server-side rendering for critical content
  • Provide fallbacks for JavaScript-disabled users
  • Test with search engine crawlers

VitePress Development Guide