---
title: "Twoslash"
description: "How to add interactive TypeScript type tooltips and error annotations to code blocks using @shikijs/twoslash with Comark."
canonical_url: "https://comark.dev/kb/twoslash"
---
# Twoslash

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

[Twoslash](https://twoslash.netlify.app) 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](https://shiki.style/guide/transformers) and wires into Comark through the [Shiki plugin](https://comark.dev/plugins/built-in/shiki).

## Installation

<tabs class="gap-0">
<tab-item label="Server" icon="i-lucide-server">
<code-group>
```bash [pnpm]
pnpm add shiki @shikijs/twoslash
```


```bash [npm]
npm install shiki @shikijs/twoslash
```


```bash [yarn]
yarn add shiki @shikijs/twoslash
```


```bash [bun]
bun add shiki @shikijs/twoslash
```
</code-group>
</tab-item>
<tab-item label="Browser" icon="i-lucide-globe">
<code-group>
```bash [pnpm]
pnpm add shiki @shikijs/twoslash twoslash-cdn
```


```bash [npm]
npm install shiki @shikijs/twoslash twoslash-cdn
```


```bash [yarn]
yarn add shiki @shikijs/twoslash twoslash-cdn
```


```bash [bun]
bun add shiki @shikijs/twoslash twoslash-cdn
```
</code-group>
</tab-item>
</tabs>

## 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:

```typescript [parse.ts]
import { parseMarkdown } from 'comark'
import shiki from 'comark/plugins/shiki'
import { transformerTwoslash } from '@shikijs/twoslash'
import githubLight from '@shikijs/themes/github-light'
import githubDark from '@shikijs/themes/github-dark'

const result = await parseMarkdown(content, {
  plugins: [
    shiki({
      themes: { light: githubLight, dark: githubDark },
      transformers: [transformerTwoslash()],
    }),
  ],
})
```

<tip>
In Nuxt, register the plugin inside your `comark` config and it runs at build time, with zero client JavaScript.
</tip>

## 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:

```typescript [App.ts]
import { createTransformerFactory, rendererRich } from '@shikijs/twoslash/core'
import { createTwoslashFromCDN } from 'twoslash-cdn'
import shiki from 'comark/plugins/shiki'
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 = shiki({
  themes: { light: githubLight, dark: githubDark },
  transformers: [transformer],
})
```

<tip>
`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.
</tip>

### Vue + Vite

```vue [App.vue]
<script setup lang="ts">
import { shallowRef, onMounted } from 'vue'
import { Markdown } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
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 = [
    shiki({
      themes: { light: githubLight, dark: githubDark },
      transformers: [transformer],
    }),
  ]
})
</script>

<template>
  <Suspense>
    <Markdown v-if="plugins" :plugins="plugins">{{ content }}</Markdown>
  </Suspense>
</template>
```

## Annotations

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:

~~~markdown
```ts twoslash
const message = "Hello from Twoslash"
//    ^?
```
~~~

### Error annotations · `@errors`

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

~~~markdown
```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:

~~~markdown
```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:

```typescript [main.ts]
import '@shikijs/twoslash/style-rich.css'
```

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

```css [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

<card icon="i-simple-icons-typescript" title="Vue + Vite Twoslash" to="https://github.com/comarkjs/comark/tree/main/examples/3.plugins/vue-vite-twoslash">
Browser-side Twoslash with CDN-fetched TypeScript types, dark mode toggle, and interactive type popups. Ready to run with `pnpm dev`.
</card>

---

- [Syntax Highlighting](https://comark.dev/plugins/built-in/shiki)


## Sitemap

See the full [sitemap](https://comark.dev/sitemap.md) for all pages.
