markdown-it-sfc-tabs-snippet

Markdown-it plugin to display Vue Single File Component as tabs snippets in markdown
260
Install
npm install markdown-it-sfc-tabs-snippet
Latest Version:0.1.1
Vue:3
License:MIT
Last Updated:Oct 17, 2026
Links: GitHub  ·  npm
Maintainer: okamidev

markdown-it-sfc-tabs-snippet

npm version CI license: MIT

Live demo →

A markdown-it plugin that imports a Vue Single File Component into your markdown as a group of code blocks — one per block the SFC declares. In VitePress, those render as tabs.

Write this:

<<<vue ./components/Counter.vue

Get a tabbed code group holding the component's <template>, <script setup> and <style>, each syntax-highlighted with its own language, all read from the real file so the docs can never drift from the code.

Install

npm i -D markdown-it-sfc-tabs-snippet

markdown-it and vue are peer dependencies — the plugin uses vue/compiler-sfc to parse the component, so the parse always matches the Vue version your project already has.

Peer Range
markdown-it ^13.0.0, ^14.0.0
vue ^3.3.0

Setup

VitePress

// .vitepress/config.ts
import { defineConfig } from 'vitepress';
import vueSnippet from 'markdown-it-sfc-tabs-snippet';

export default defineConfig({
    markdown: {
        config: (md) => {
            md.use(vueSnippet);
        },
    },
});

No options needed: the plugin reads the page path VitePress passes in the render environment, so relative paths and @ resolve on their own, and it registers each imported SFC as a page dependency so editing the component hot-reloads the page.

Plain markdown-it

import MarkdownIt from 'markdown-it';
import vueSnippet from 'markdown-it-sfc-tabs-snippet';

const md = new MarkdownIt().use(vueSnippet, { root: process.cwd() });

// `path` tells the plugin which page is rendering, so relative paths resolve against it.
md.render(source, { path: '/docs/guide/index.md' });

Outside VitePress there is no code-group renderer, so the wrapper falls back to a plain <div> around the code blocks rather than tabs. The blocks themselves are ordinary fence tokens and render normally.

Syntax

<<<vue ./Counter.vue           relative to the markdown file
<<<vue ../shared/Button.vue    parent directories work
<<<vue @/components/Card.vue   `@` is the markdown root

The operator must start the line (indenting it four spaces or more makes it an ordinary code block, as usual in markdown). Everything after <<<vue is the path, trimmed.

A leading @ is replaced by the root option, or by the markdown root deduced from the render environment. Every other path is resolved against the directory of the markdown file being rendered.

If the path does not resolve to a file, the plugin emits a code block reading Vue snippet path not found: <path> instead of throwing, so one bad path doesn't fail your build.

Blocks and tabs

Each block in the SFC becomes one tab, in this order:

SFC block Tab title Language
<template> template vue-html
<script setup> script setup lang attr, else js
<script> script lang attr, else js
<style> style lang attr, else css
<style scoped> style scoped lang attr, else css
custom block the block name lang attr, else txt

Multiple <style> blocks and custom blocks (<i18n>, <docs>, …) each get their own tab, in source order. Blocks the file doesn't declare are simply absent — no empty tabs.

Content is cleaned up on the way in: surrounding blank lines are dropped, and the indentation shared by every line is removed, so <template> content isn't shifted right by the tag it sits in.

So this component:

<template>
    <button @click="count++">Count is {{ count }}</button>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const count = ref(0);
</script>

<style scoped>
button {
    color: rebeccapurple;
}
</style>

produces three tabs — template (vue-html), script setup (ts) and style scoped (css) — each holding that block's contents, dedented.

Options

md.use(vueSnippet, {
    root: '/abs/path/to/docs',
    containerName: 'code-group',
});

root

Type: string · Default: deduced from the render environment, falling back to process.cwd()

Directory the @ path prefix points to. VitePress supplies enough information to deduce this, so you normally leave it unset; set it when driving markdown-it yourself.

containerName

Type: string · Default: 'code-group'

Name of the container whose tokens wrap the code blocks. The default matches VitePress's ::: code-group, which is what renders the group as tabs. Point it at another container if you have your own renderer; a container with no renderer falls back to plain <div> wrappers.

How it hooks in

The plugin adds a block rule named vue-snippet to markdown-it's ruler. Placement matters: VitePress ships its own <<< snippet operator, and since it tests only for three <, it would otherwise claim <<<vue lines and treat vue ./Counter.vue as the path. So the rule inserts itself before VitePress's snippet rule when that rule is present, and before the built-in fence rule otherwise.

Requirements

Node 20 or newer. The plugin only reads files at render time — nothing is written, and the SFC is never compiled, only parsed.

Contributing

Issues and pull requests are welcome. CONTRIBUTING.md covers the development setup, the available scripts and the commit conventions. Please follow the Code of Conduct, and report security problems privately as described in SECURITY.md.

License

MIT

Related Packages

@mdit-vue/plugin-sfc

A markdown-it plugin to help transforming markdown to vue sfc

1,849,811 221
@loopstates/vitepress-plugin-linten

Official VitePress & Vite plugin for auditing llms.txt link health and compiling...

180 0
vitepress-plugin-chartjs

Chart.js plugin for VitePress - render charts from markdown code blocks

1,876 1
@mussi/vitepress-theme

A lightweight VitePress theme extension for Ruby project documentation.

792 6
@mdit-vue/plugin-component

A markdown-it plugin to allow vue components in markdown

5,900,143 221

Version History

Version Released Vue License
0.1.1 ^3.3.0 MIT
0.1.0 ^3.3.0 MIT