Twoslash
Twoslash runs the real TypeScript compiler on your code blocks to produce inline type tooltips, expected error markers, and hidden setup code — all at parse time, with zero client-side JavaScript.
It is implemented as a Shiki transformer and wires into Comark through the highlight plugin.
Installation
# Server-side (Node.js, Nuxt SSR, static build)npm install shiki @shikijs/twoslash# Browser (Vite SPA, client-side rendering)npm install shiki @shikijs/twoslash twoslash-cdn# Server-sidepnpm add shiki @shikijs/twoslash# Browserpnpm add shiki @shikijs/twoslash twoslash-cdn# Server-sideyarn add shiki @shikijs/twoslash# Browseryarn add shiki @shikijs/twoslash twoslash-cdnSetup
Server-side
In a Node.js context — SSR, static site generation, or a build-time plugin — TypeScript's own file system is available, so use @shikijs/twoslash directly:
import { parse } from 'comark'import highlight from 'comark/plugins/highlight'import { transformerTwoslash } from '@shikijs/twoslash'import githubLight from '@shikijs/themes/github-light'import githubDark from '@shikijs/themes/github-dark'const result = await parse(content, { plugins: [ highlight({ themes: { light: githubLight, dark: githubDark }, transformers: [transformerTwoslash()], }), ],})Nuxt tip — Register the plugin inside your comark config and it runs at build time. Zero client JavaScript.Browser
In the browser there is no filesystem, so TypeScript cannot load its type definitions the normal way. Use twoslash-cdn to fetch them over CDN instead:
import { createTransformerFactory, rendererRich } from '@shikijs/twoslash/core'import { createTwoslashFromCDN } from 'twoslash-cdn'import highlight from 'comark/plugins/highlight'import githubLight from '@shikijs/themes/github-light'import githubDark from '@shikijs/themes/github-dark'// Fetch TypeScript stdlib from CDN once at startup (~500 KB)const twoslash = createTwoslashFromCDN()await twoslash.init()const transformer = createTransformerFactory(twoslash.runSync)({ explicitTrigger: true, // only compile blocks with `twoslash` in their meta string renderer: rendererRich(),})const plugin = highlight({ themes: { light: githubLight, dark: githubDark }, transformers: [transformer],})Tip —explicitTrigger: truelimits compilation to code blocks tagged withtwoslashin their meta string — plaintsblocks are left untouched. Pair with@shikijs/twoslash/style-rich.cssfor the default popup styles.
Vue + Vite
<script setup lang="ts">import { shallowRef, onMounted } from 'vue'import { Comark } from '@comark/vue'import highlight from '@comark/vue/plugins/highlight'import { createTransformerFactory, rendererRich } from '@shikijs/twoslash/core'import { createTwoslashFromCDN } from 'twoslash-cdn'import githubLight from '@shikijs/themes/github-light'import githubDark from '@shikijs/themes/github-dark'import '@shikijs/twoslash/style-rich.css'import type { ComarkPlugin } from 'comark'const plugins = shallowRef<ComarkPlugin[] | null>(null)onMounted(async () => { const twoslash = createTwoslashFromCDN() await twoslash.init() const transformer = createTransformerFactory(twoslash.runSync)({ explicitTrigger: true, renderer: rendererRich(), }) plugins.value = [ highlight({ themes: { light: githubLight, dark: githubDark }, transformers: [transformer], }), ]})</script><template> <Suspense> <Comark v-if="plugins" :plugins="plugins">{{ content }}</Comark> </Suspense></template>Annotations Reference
Add annotations directly in your code block — they are compiled and stripped from the output:
| Annotation | Where | Effect |
|---|---|---|
^? | After a symbol | Show inferred type in a hover popup |
// @errors: N … | Top of block | Expect TypeScript error codes; mark others as unexpected |
// @noErrors | Top of block | Suppress all type errors silently |
// ---cut--- | Any line | Everything above is compiled but hidden from output |
// ---cut-after--- | Any line | Everything below is compiled but hidden from output |
// @filename: foo.ts | Top of block | Treat block as a named virtual file (for multi-file examples) |
Type Hover · ^?
Point an arrow at any identifier to show its inferred type in a popup:
```ts twoslashconst message = "Hello from Twoslash"// ^?```Error Annotations · @errors
Document intentional type errors — great for teaching correct API usage:
```ts twoslash// @errors: 2322let count: number = "not a number"```Hide Setup · // ---cut---
Code above the cut compiles but is hidden from readers — useful for imports and shared types:
```ts twoslashinterface User { id: number; name: string }function getUser(id: number): User { return { id, name: 'Alice' }}// ---cut---const user = getUser(42)// ^?```Styling
Import the bundled stylesheet for popup styles, then override variables to match your theme:
import '@shikijs/twoslash/style-rich.css'Dark mode overrides (using Nuxt UI CSS variables as an example):
.dark .twoslash-popup-container { background: var(--ui-bg-elevated) !important; border-color: var(--ui-border) !important;}.dark .twoslash-popup-code span { color: var(--shiki-dark) !important;}/* Prevent nested <pre> inside popup from inheriting block styles */pre .twoslash-hover pre { margin: 0; padding: 0; background: transparent !important; border: none;}pre .twoslash-hover pre .line { display: inline; }Live Example
Vue + Vite Twoslash
pnpm dev.See Also
- Syntax Highlighting —
transformersoption and all highlight settings - Shiki Transformers — full transformer API
- Twoslash Documentation — annotation reference