Knowledge Base
How to add interactive TypeScript type tooltips and error annotations to code blocks using @shikijs/twoslash with Comark.

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

Setup

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:

parse.ts
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:

App.ts
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],})
TipexplicitTrigger: true limits compilation to code blocks tagged with twoslash in their meta string — plain ts blocks are left untouched. Pair with @shikijs/twoslash/style-rich.css for the default popup styles.

Vue + Vite

App.vue
<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:

AnnotationWhereEffect
^?After a symbolShow inferred type in a hover popup
// @errors: N …Top of blockExpect TypeScript error codes; mark others as unexpected
// @noErrorsTop of blockSuppress all type errors silently
// ---cut---Any lineEverything above is compiled but hidden from output
// ---cut-after---Any lineEverything below is compiled but hidden from output
// @filename: foo.tsTop of blockTreat 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:

main.ts
import '@shikijs/twoslash/style-rich.css'

Dark mode overrides (using Nuxt UI CSS variables as an example):

styles.css
.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

Browser-side Twoslash with CDN-fetched TypeScript types, dark mode toggle, and interactive type popups — ready to run with pnpm dev.

See Also

Copyright © 2026