From slidev-complete
Adds interactivity to Slidev slides using built-in Vue components like Arrow, Toc, Youtube, Transform or custom ones in Markdown.
npx claudepluginhub yoanbernabeu/slidev-skills --plugin slidev-getting-startedThis skill uses the workspace's default tool permissions.
This skill covers using Vue components in Slidev, including all built-in components and how to create custom interactive elements for your presentations.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
This skill covers using Vue components in Slidev, including all built-in components and how to create custom interactive elements for your presentations.
Components can be used directly in Markdown:
# My Slide
<MyComponent />
<Counter :start="5" />
Draws an arrow between points.
<Arrow x1="10" y1="20" x2="100" y2="200" />
<Arrow
x1="10"
y1="20"
x2="100"
y2="200"
color="#f00"
width="3"
/>
Props:
x1, y1: Start coordinatesx2, y2: End coordinatescolor: Arrow colorwidth: Line widthDraggable arrow (useful for presentations).
<VDragArrow />
Automatically adjusts font size to fit container.
<AutoFitText :max="200" :min="50" modelValue="My Text" />
Props:
max: Maximum font sizemin: Minimum font sizemodelValue: Text contentRenders different content based on theme.
<LightOrDark>
<template #light>
<img src="/logo-dark.png" />
</template>
<template #dark>
<img src="/logo-light.png" />
</template>
</LightOrDark>
Navigation link to other slides.
<Link to="42">Go to slide 42</Link>
<Link to="/intro">Go to intro</Link>
Display slide numbers.
Slide <SlideCurrentNo /> of <SlidesTotal />
Generates a table of contents.
<Toc />
<Toc maxDepth="2" />
<Toc mode="onlyCurrentTree" />
Props:
maxDepth: Maximum heading depthmode: Display mode (all, onlyCurrentTree, onlySiblings)Applies CSS transforms.
<Transform :scale="1.5">
<div>Scaled content</div>
</Transform>
<Transform :scale="0.8" :rotate="10">
Rotated and scaled
</Transform>
Props:
scale: Scale factorrotate: Rotation in degreesEmbeds a tweet.
<Tweet id="1234567890" />
<Tweet id="1234567890" scale="0.8" />
Embeds a YouTube video.
<Youtube id="dQw4w9WgXcQ" />
<Youtube id="dQw4w9WgXcQ" width="560" height="315" />
Props:
id: YouTube video IDwidth, height: DimensionsEmbeds a video file.
<SlidevVideo v-click autoplay controls>
<source src="/video.mp4" type="video/mp4" />
</SlidevVideo>
Props:
autoplay: Auto-play on slide entercontrols: Show video controlsloop: Loop videoConditional rendering based on context.
<RenderWhen context="slide">
Only visible in slide view
</RenderWhen>
<RenderWhen context="presenter">
Only visible in presenter view
</RenderWhen>
Context options: slide, presenter, previewNext, print
Makes elements draggable.
<VDrag>
<div class="p-4 bg-blue-500 text-white">
Drag me!
</div>
</VDrag>
<VDrag :initialX="100" :initialY="50">
Positioned draggable
</VDrag>
Reveals on click.
<v-click>
Revealed on first click
</v-click>
<v-click at="2">
Revealed on second click
</v-click>
Reveals children sequentially.
<v-clicks>
- First item
- Second item
- Third item
</v-clicks>
Props:
depth: Depth for nested listsevery: Items per clickReveals with the previous element.
<v-click>First</v-click>
<v-after>Appears with first</v-after>
Switches between content based on clicks.
<v-switch>
<template #1>Step 1 content</template>
<template #2>Step 2 content</template>
<template #3>Step 3 content</template>
</v-switch>
Create components/Counter.vue:
<script setup>
import { ref } from 'vue'
const props = defineProps({
start: {
type: Number,
default: 0
}
})
const count = ref(props.start)
</script>
<template>
<div class="counter">
<button @click="count--">-</button>
<span class="count">{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<style scoped>
.counter {
display: flex;
align-items: center;
gap: 1rem;
}
button {
padding: 0.5rem 1rem;
font-size: 1.5rem;
cursor: pointer;
}
.count {
font-size: 2rem;
min-width: 3rem;
text-align: center;
}
</style>
Usage:
# Interactive Counter
<Counter :start="10" />
<!-- components/Card.vue -->
<script setup>
defineProps({
title: String,
color: {
type: String,
default: 'blue'
}
})
</script>
<template>
<div :class="`card card-${color}`">
<h3 v-if="title">{{ title }}</h3>
<slot />
</div>
</template>
<style scoped>
.card {
padding: 1.5rem;
border-radius: 0.5rem;
margin: 1rem 0;
}
.card-blue { background: #3b82f6; color: white; }
.card-green { background: #22c55e; color: white; }
.card-red { background: #ef4444; color: white; }
</style>
Usage:
<Card title="Important" color="red">
This is a red card with important content.
</Card>
<!-- components/ProgressBar.vue -->
<script setup>
import { computed } from 'vue'
import { useNav } from '@slidev/client'
const { currentSlideNo, total } = useNav()
const progress = computed(() =>
(currentSlideNo.value / total.value) * 100
)
</script>
<template>
<div class="progress-bar">
<div
class="progress"
:style="{ width: `${progress}%` }"
/>
</div>
</template>
<style scoped>
.progress-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 4px;
background: #e5e7eb;
}
.progress {
height: 100%;
background: #3b82f6;
transition: width 0.3s;
}
</style>
<!-- components/CodeDemo.vue -->
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
code: String,
language: {
type: String,
default: 'javascript'
}
})
const output = ref('')
const error = ref('')
const run = () => {
try {
output.value = eval(props.code)
error.value = ''
} catch (e) {
error.value = e.message
output.value = ''
}
}
</script>
<template>
<div class="code-demo">
<pre><code>{{ code }}</code></pre>
<button @click="run">Run</button>
<div v-if="output" class="output">{{ output }}</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
Access navigation state:
<script setup>
import { useNav } from '@slidev/client'
const {
currentSlideNo, // Current slide number
total, // Total slides
next, // Go to next
prev, // Go to previous
go // Go to specific slide
} = useNav()
</script>
Access slide context:
<script setup>
import { useSlideContext } from '@slidev/client'
const {
$slidev, // Global context
$clicks, // Current click count
$page // Current page number
} = useSlideContext()
</script>
Appears above all slides:
<!-- global-top.vue -->
<template>
<div class="absolute top-4 right-4">
<img src="/logo.png" class="h-8" />
</div>
</template>
Appears below all slides:
<!-- global-bottom.vue -->
<template>
<footer class="absolute bottom-4 left-4 text-sm opacity-50">
© 2025 My Company
</footer>
</template>
<div class="fixed bottom-4 right-4 text-sm">
<SlideCurrentNo /> / <SlidesTotal />
</div>
<!-- components/SocialLinks.vue -->
<template>
<div class="flex gap-4">
<a href="https://twitter.com/..." target="_blank">
<carbon-logo-twitter class="text-2xl" />
</a>
<a href="https://github.com/..." target="_blank">
<carbon-logo-github class="text-2xl" />
</a>
</div>
</template>
<!-- components/QRCode.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import QRCodeLib from 'qrcode'
const props = defineProps({
url: String,
size: { type: Number, default: 200 }
})
const qrDataUrl = ref('')
onMounted(async () => {
qrDataUrl.value = await QRCodeLib.toDataURL(props.url, {
width: props.size
})
})
</script>
<template>
<img :src="qrDataUrl" :width="size" :height="size" />
</template>
When creating components, provide:
COMPONENT: [name]
PURPOSE: [what it does]
FILE: components/[Name].vue
---
<script setup>
[script content]
</script>
<template>
[template content]
</template>
<style scoped>
[styles]
</style>
---
USAGE IN SLIDES:
```markdown
<[Name] prop="value" />
PROPS: