Knowledge Base

Twoslash

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

pnpm add shiki @shikijs/twoslash

Server

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()],
    }),
  ],
})
In Nuxt, 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'

const twoslash = createTwoslashFromCDN()
await twoslash.init()

const transformer = createTransformerFactory(twoslash.runSync)({
  explicitTrigger: true,
  renderer: rendererRich(),
})

const plugin = highlight({
  themes: { light: githubLight, dark: githubDark },
  transformers: [transformer],
})
explicitTrigger: 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

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 twoslash
const message = "Hello from Twoslash"
//    ^?
```

Error Annotations · @errors

Document intentional type errors — great for teaching correct API usage:

```ts twoslash
// @errors: 2322
let 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 twoslash
interface 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.