---
title: "Twoslash Type Hover"
description: "Fumadocs provides the fumadocs-twoslash package to integrate TypeScript Twoslash analysis directly into code blocks via Shiki transformers. It leverages a lazy-loaded Twoslash compiler instance wit..."
last_updated: "2026-10-12T07:53:59.736226+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tooling/twoslash-type-hover"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/twoslash/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts)
- [packages/base-ui/css/lib/shiki.css](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/css/lib/shiki.css)
- [packages/radix-ui/css/lib/shiki.css](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/css/lib/shiki.css)
- [packages/core/src/highlight/shiki/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/index.ts)
- [packages/api-docs/src/components/schema/client.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/api-docs/src/components/schema/client.tsx)
- [packages/twoslash/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/package.json)
- [packages/twoslash/styles/twoslash.css](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/styles/twoslash.css)
- [packages/shadcn/src/rsc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/shadcn/src/rsc.tsx)
- [packages/typescript/src/markdown.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/typescript/src/markdown.ts)
- [packages/core/src/mdx-plugins/rehype-code/shiki.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/shiki.ts)
- [packages/typescript/src/ui/auto-type-table.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/typescript/src/ui/auto-type-table.tsx)
- [packages/twoslash/tsconfig.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/tsconfig.json)
- [packages/base-ui/src/components/dynamic-codeblock.core.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dynamic-codeblock.core.tsx)
- [packages/core/src/highlight/shiki/react.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/react.ts)
- [packages/asyncapi/src/ui/index.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/ui/index.tsx)
- [packages/core/src/highlight/shiki/full.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/full.ts)
- [packages/typescript/src/lib/remark-auto-type-table.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/typescript/src/lib/remark-auto-type-table.ts)
- [packages/base-ui/src/components/codeblock.rsc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/codeblock.rsc.tsx)
- [packages/radix-ui/src/components/dynamic-codeblock.core.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dynamic-codeblock.core.tsx)
- [packages/shadcn/src/manual-installation.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/shadcn/src/manual-installation.ts)
- [packages/base-ui/src/components/dynamic-codeblock.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/src/components/dynamic-codeblock.tsx)
- [packages/openapi/src/ui/base.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/ui/base.tsx)
- [packages/core/src/highlight/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/index.ts)
- [packages/core/src/highlight/client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/client.ts)
- [packages/mdx/src/loaders/mdx/remark-include.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/loaders/mdx/remark-include.ts)
- [packages/asyncapi/src/ui/components/codeblock.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/asyncapi/src/ui/components/codeblock.tsx)
- [packages/twoslash/src/ui/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/ui/index.ts)
- [packages/radix-ui/src/components/codeblock.rsc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/codeblock.rsc.tsx)
- [packages/openapi/src/ui/components/codeblock.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/openapi/src/ui/components/codeblock.tsx)
- [packages/core/src/mdx-plugins/rehype-code.core.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code.core.ts)
</details>

## Overview

Fumadocs provides the `fumadocs-twoslash` package to integrate TypeScript Twoslash analysis directly into code blocks via Shiki transformers. It leverages a lazy-loaded Twoslash compiler instance with bundled module resolution to analyze TypeScript snippets and annotate code blocks with interactive type hover cards, error diagnostics, and completion lists.

Sources: [packages/twoslash/src/index.ts:28-52](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L28-L52), [packages/twoslash/package.json:2-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/package.json#L2-L4)

## Package architecture and dependencies

### Public entry points and file configuration

The package `fumadocs-twoslash` exposes multiple modular entry points and distribution targets configured in its package manifest. The primary entry point resolves to `./dist/index.js` with TypeScript definitions at `./dist/index.d.ts`, while stylesheets, UI runtime components, and file-system caching targets are exposed through specific export paths.

| Export Subpath | Target File | Type Definition | Purpose |
| -------------- | ----------- | --------------- | ------- |
| `.` | `./dist/index.js` | `./dist/index.d.ts` | Main Shiki transformer and transformer options entry |
| `./ui` | `./dist/ui/index.js` | `./dist/ui/index.d.ts` | Client-side popup and trigger components |
| `./cache-fs` | `./dist/cache-fs.js` | `./dist/cache-fs.d.ts` | File-system caching utility for Twoslash compilation |
| `./twoslash.css` | `./styles/twoslash.css` | — | Stylesheet bundle for type hover cards and diagnostics |

Sources: [packages/twoslash/package.json:20-34](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/package.json#L20-L34)

The build process is managed via `tsdown` with watch mode support in development, linting provided by `oxlint`, and type checking executed through `tsc --noEmit`. The TypeScript configuration extends `tsconfig/react-library.json`, maps package internal imports via `@/*` to `./src/*`, and includes compiler type references for Node and Fumadocs rehype code plugins.

Sources: [packages/twoslash/package.json:38-44](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/package.json#L38-L44), [packages/twoslash/tsconfig.json:1-8](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/tsconfig.json#L1-L8)

### Runtime and peer dependencies

The runtime package dependencies combine [Radix UI primitives](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/radix-ui-primitives), Shiki Twoslash integrations, Markdown abstract syntax tree utilities, and Twoslash compiler tooling. Peer dependencies establish integration constraints with React, Fumadocs core and UI packages, and Shiki.

| Dependency Name | Version Constraint | Category | Purpose |
| --------------- | ------------------ | -------- | ------- |
| `@radix-ui/react-popover` | `^1.1.16` | Dependency | Accessible popover positioning primitives |
| `@shikijs/twoslash` | `^4.2.0` | Dependency | Shiki integration factory for Twoslash |
| `mdast-util-from-markdown` | `^2.0.3` | Dependency | Parsing Markdown strings into MDAST trees |
| `mdast-util-gfm` | `^3.1.0` | Dependency | GitHub Flavored Markdown support for MDAST |
| `mdast-util-to-hast` | `^13.2.1` | Dependency | Transforming MDAST nodes into HAST elements |
| `tailwind-merge` | `^3.6.0` | Dependency | Utility for merging Tailwind CSS classes |
| `twoslash` | `^0.3.8` | Dependency | Core TypeScript compiler analysis engine |
| `fumadocs-core` | `^16.7.16` | Peer Dependency | Core documentation processing and rehype plugins |
| `fumadocs-ui` | `^16.7.16` | Peer Dependency | UI layout components and design primitives |
| `react` / `react-dom` | `^19.2.0` | Peer Dependency | React runtime framework |
| `shiki` | `4.x.x` | Peer Dependency | Syntax highlighter engine |

Sources: [packages/twoslash/package.json:45-53](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/package.json#L45-L53), [packages/twoslash/package.json:66-73](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/package.json#L66-L73)

> [!NOTE]
> The `twoslash` compiler instance is initialized lazily inside `transformerTwoslash` to ensure compatibility with serverless runtime environments where top-level compiler instantiation might fail or execute prematurely.

Sources: [packages/twoslash/src/index.ts:35-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L35-L36)

## Shiki transformer integration

### Transformer initialization and lazy compilation

The `transformerTwoslash` function acts as the integration boundary between Fumadocs code blocks and the `@shikijs/twoslash` transformer factory. Designed to operate alongside the `rehype-code` plugin, it accepts optional `TransformerTwoslashOptions` and sets an ignore class `nd-copy-ignore` on generated elements to prevent copy actions from capturing type metadata. To support serverless environments, the underlying Twoslash compiler instance is instantiated lazily via `lazyInstance()`, configuring default compiler options with `ModuleResolutionKind.Bundler` (value `100`), clearing any preset `baseUrl`, and merging any user-provided `twoslashOptions`.

```typescript
/**
 * This transformer **must** be used with the `rehype-code` plugin of Fumadocs.
 */
export function transformerTwoslash(_options: TransformerTwoslashOptions = {}): ShikiTransformer {
  const ignoreClass = 'nd-copy-ignore';
  const { twoslashOptions = {}, rendererRich: rendererOptions, ...rest } = _options;

  // lazy load Twoslash instance so it works on serverless platforms
  function lazyInstance(): TwoslashInstance {
    function get() {
      return (cachedInstance ??= createTwoslasher({
        ...twoslashOptions,
        compilerOptions: {
          moduleResolution: 100 satisfies ModuleResolutionKind.Bundler,
          baseUrl: undefined,
          ...twoslashOptions.compilerOptions,
        },
      }));
    }

    const wrapper: TwoslashInstance = (...args) => get()(...args);

    wrapper.getCacheMap = () => get().getCacheMap();
    return wrapper;
  }
```
Sources: [packages/twoslash/src/index.ts:28-52](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L28-L52)

> [!NOTE]
> Setting `moduleResolution` to `100` (`ModuleResolutionKind.Bundler`) ensures that type analysis resolves module imports correctly for modern bundler environments without requiring an explicit `baseUrl` setting.

Sources: [packages/twoslash/src/index.ts:40-44](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L40-L44)

### Renderer and query AST interception

The transformer initializes `rendererRich` from `@shikijs/twoslash` with custom HAST node mappings for hover tokens, popup contents, and documentation styling. It customizes `renderer.lineQuery` by intercepting query AST results and wrapping the child nodes inside a new `span` element to ensure proper styling and layout integration in rendered code blocks. Finally, it invokes `createTransformerFactory` with the lazy compiler instance, the customized renderer, and an `explicitTrigger` flag set to `true`.

```typescript
  const renderer = rendererRich({
    classExtra: ignoreClass,
    queryRendering: 'line',
    renderMarkdown,
    renderMarkdownInline,
    ...rendererOptions,
    hast: {
      hoverToken: {
        tagName: 'Popup',
      },
      hoverPopup: {
        tagName: 'PopupContent',
        properties: {
          class: ignoreClass,
        },
      },
      hoverCompose: ({ popup, token }) => [
        popup,
        {
          type: 'element',
          tagName: 'PopupTrigger',
          properties: {},
          children: [token],
        },
      ],
      popupDocs: {
        class: 'prose twoslash-popup-docs',
      },
      popupTypes: {
        tagName: 'div',
        class: 'twoslash shiki fd-codeblock prose-no-margin',
        children: (v) => {
          if (v.length === 1 && v[0].type === 'element' && v[0].tagName === 'pre') return v;

          return [
            {
              type: 'element',
              tagName: 'code',
              properties: {
                class: 'twoslash-popup-code',
              },
              children: v,
            },
          ];
        },
      },
      popupDocsTags: {
        class: 'prose twoslash-popup-docs twoslash-popup-docs-tags',
      },
      nodesHighlight: {
        class: 'highlighted-word twoslash-highlighted',
      },
      ...rendererOptions?.hast,
    },
  });

  const fn = renderer.lineQuery!;
  renderer.lineQuery = function (this: ShikiTransformerContext, ...args) {
    const result = fn.call(this, ...args);
    // this may break if Shiki updates, need more attention
    // @ts-expect-error -- extract offset
    const child = result[0].children[0];
    // @ts-expect-error -- attend offset as span
    result[0].children[0] = {
      type: 'element',
      tagName: 'span',
      children: [child],
    };
    return result;
  };
  return createTransformerFactory(
    lazyInstance(),
    renderer,
  )({
    explicitTrigger: true,
    ...rest,
  });
}
```
Sources: [packages/twoslash/src/index.ts:54-131](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L54-L131)

> [!WARNING]
> The `renderer.lineQuery` hook depends on internal Shiki AST structures; updates to upstream Shiki releases can break the AST offset extraction logic if the node hierarchy changes.

Sources: [packages/twoslash/src/index.ts:113-116](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L113-L116)

## Markdown parsing in type hover

### MDAST and HAST transformation pipelines

```typescript
function renderMarkdown(this: ShikiTransformerContextCommon, md: string): ElementContent[] {
  const mdast = fromMarkdown(
    md.replace(/{@link (?<link>[^}]*)}/g, '$1'), // replace jsdoc links
    { mdastExtensions: [gfmFromMarkdown()] },
  );

  const onCode = (lang: string, node: Code) => {
    return this.codeToHast(node.value, {
      ...this.options,
      transformers: [],
      meta: node.meta
        ? {
            __raw: node.meta,
          }
        : {},
      lang,
    }).children[0] as Element;
  };

  return (
    toHast(mdast, {
      handlers: {
        code: (state, node: Code) => {
          const lang = node.lang;
          if (!lang) return defaultHandlers.code(state, node);

          try {
            return onCode(lang, node);
          } catch (e) {
            const def = defaultHandlers.code(state, node);

            if (e instanceof ShikiError) {
              this.meta._fd_postprocess ??= [];
              this.meta._fd_postprocess.push(async ({ highlighter }) => {
                await highlighter.loadLanguage(lang as never);
                Object.assign(def, onCode(lang, node));
              });

              return def;
            }

            if (e instanceof Error) {
              console.error(
                `[fumadocs-twoslash] encountered an error when highlighting codeblock in a Twoslash popup: ${e.message}`,
              );
            }

            return def;
          }
        },
      },
    }) as Element
  ).children;
}
```
Sources: [packages/twoslash/src/index.ts:133-186](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L133-L186)

The `renderMarkdown` function parses JSDoc documentation strings and markdown descriptions into HAST element arrays for display inside Twoslash hover popups. It normalizes input strings by stripping JSDoc link tags matching `/{@link (?<link>[^}]*)}/g` down to their raw link text representation. Using `fromMarkdown` with `gfmFromMarkdown` extensions, it builds an MDAST tree that converts through `toHast` with custom code block handlers.

```typescript
export function markdownRenderer(options?: ShikiOptions): MarkdownRenderer {
  const processor = remark()
    .use(remarkGfm)
    .use(remarkRehype)
    .use(rehypeCode, {
      langs: ['ts', 'tsx'],
      // disable default transformers & meta parser
      transformers: [],
      parseMetaString: undefined,
      ...options,
    });
  return {
    async renderTypeToHast(type) {
      const nodes = await highlightHast(type, {
        lang: 'ts',
        structure: 'inline',
        defaultColor: false,
        ...options,
      });

      return {
        type: 'element',
        tagName: 'span',
        properties: {
          class: 'shiki',
        },
        children: [
          {
            type: 'element',
            tagName: 'code',
            properties: {},
            children: nodes.children as ElementContent[],
          },
        ],
      };
    },
    renderMarkdownToHast(md) {
      md = md.replace(/{@link (?<link>[^}]*)}/g, '$1'); // replace jsdoc links

      return processor.run(processor.parse(md));
    },
  };
```
Sources: [packages/typescript/src/markdown.ts:16-57](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/typescript/src/markdown.ts#L16-L57)

Similarly, the `markdownRenderer` factory in `@fumadocs/typescript` sets up a remark processor running `remarkGfm`, `remarkRehype`, and `rehypeCode` configured specifically for TypeScript and TSX code blocks. Its `renderTypeToHast` function wraps highlighted type nodes inside structured `span` and `code` HAST elements, while `renderMarkdownToHast` parses markdown strings after stripping `{@link …}` tags.

> [!NOTE]
> JSDoc link expressions are preprocessed across both markdown pipeline implementations using regular expression substitution to replace `{@link identifier}` tags with their inner text before MDAST parsing begins.

Sources: [packages/twoslash/src/index.ts:134-135](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L134-L135), [packages/typescript/src/markdown.ts:52-53](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/typescript/src/markdown.ts#L52-L53)

### Code block handling and Shiki error recovery

```typescript
const onCode = (lang: string, node: Code) => {
  return this.codeToHast(node.value, {
    ...this.options,
    transformers: [],
    meta: node.meta
      ? {
          __raw: node.meta,
        }
      : {},
    lang,
  }).children[0] as Element;
};
```
Sources: [packages/twoslash/src/index.ts:139-150](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L139-L150)

Within `renderMarkdown`, the `onCode` helper function intercepts markdown code blocks and invokes `this.codeToHast` using the current transformation context. It passes raw meta strings under `meta.__raw` and clears active transformers to prevent recursive transformer execution inside hover documentation blocks.

```typescript
  try {
    return onCode(lang, node);
  } catch (e) {
    const def = defaultHandlers.code(state, node);

    if (e instanceof ShikiError) {
      this.meta._fd_postprocess ??= [];
      this.meta._fd_postprocess.push(async ({ highlighter }) => {
        await highlighter.loadLanguage(lang as never);
        Object.assign(def, onCode(lang, node));
      });

      return def;
    }

    if (e instanceof Error) {
      console.error(
        `[fumadocs-twoslash] encountered an error when highlighting codeblock in a Twoslash popup: ${e.message}`,
      );
    }

    return def;
  }
},
```
Sources: [packages/twoslash/src/index.ts:159-182](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L159-L182)

When code block highlighting throws a `ShikiError`, the handler falls back to standard handlers while registering a post-processing task on `this.meta._fd_postprocess`. This deferred task asynchronously loads the missing language via `highlighter.loadLanguage(lang)` and updates the HAST element in place. General `Error` instances encountered during popup code block highlighting are logged to `console.error` with a `[fumadocs-twoslash]` prefix.

> [!WARNING]
> If a requested language is missing when highlighting code blocks within a Twoslash popup, the transformer catches the resulting `ShikiError` and defers language loading to a post-processing phase rather than crashing the rendering pipeline.

Sources: [packages/twoslash/src/index.ts:164-172](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L164-L172)

### Inline markdown and parameter tag formatting

```typescript
function renderMarkdownInline(
  this: ShikiTransformerContextCommon,
  md: string,
  context?: string,
): ElementContent[] {
  const text = context === 'tag:param' ? md.replace(/^(?<link>[\w$-]+)/, '`$1` ') : md;

  const children = renderMarkdown.call(this, text);
  if (children.length === 1 && children[0].type === 'element' && children[0].tagName === 'p')
    return children[0].children;
  return children;
}
```
Sources: [packages/twoslash/src/index.ts:188-199](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts#L188-L199)

The `renderMarkdownInline` function formats inline documentation descriptions. When the context parameter matches `'tag:param'`, it applies a regex replacement `/^(?<link>[\w$-]+)/` that wraps the parameter name in backticks followed by a space. After delegating to `renderMarkdown`, it unwraps single paragraph nodes, returning their inner children directly if the result consists of exactly one paragraph element.

## Client popup components and triggers

### Client entry points and popup re-exports

```typescript
'use client';
export * from './popup';
```
Sources: [packages/twoslash/src/ui/index.ts:1-2](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/ui/index.ts#L1-L2)

The client user interface module exports all components and triggers from the `./popup` module. It is marked with the `'use client'` directive to ensure correct execution boundaries in React server-client component trees.

> [!NOTE]
> Client entry points require the `'use client'` directive because Twoslash hover cards and popup elements depend on browser-side state and interaction handlers.

Sources: [packages/twoslash/src/ui/index.ts:1-2](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/ui/index.ts#L1-L2)

## Twoslash theme and layout styling

### CSS variable themes and error decoration tokens

```css
/* ===== Basic ===== */
@theme {
  --color-twoslash-error-foreground: #d45656;
  --color-twoslash-error: #d4565620;
  --color-twoslash-tag-foreground: #3772cf;
  --color-twoslash-tag: #3772cf20;
  --color-twoslash-tag-warn-foreground: #c37d0d;
  --color-twoslash-tag-warn: #c37d0d20;
  --color-twoslash-tag-annotate-foreground: #1ba673;
  --color-twoslash-tag-annotate: #1ba67320;
}
```
Sources: [packages/twoslash/styles/twoslash.css:1-11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/styles/twoslash.css#L1-L11)

The Twoslash CSS module defines color tokens within an `@theme` block. These variables govern error highlights, tag decorations, and custom warning/annotation states across code blocks.

| CSS Variable | Default Value | Purpose |
| --- | --- | --- |
| `--color-twoslash-error-foreground` | `#d45656` | Text and border color for error diagnostics |
| `--color-twoslash-error` | `#d4565620` | Background tint for error callouts and lines |
| `--color-twoslash-tag-foreground` | `#3772cf` | Foreground color for custom documentation tags |
| `--color-twoslash-tag` | `#3772cf20` | Background tint for custom documentation tags |
| `--color-twoslash-tag-warn-foreground` | `#c37d0d` | Foreground color for warning tag lines |
| `--color-twoslash-tag-warn` | `#c37d0d20` | Background tint for warning tag lines |
| `--color-twoslash-tag-annotate-foreground` | `#1ba673` | Foreground color for annotation tag lines |
| `--color-twoslash-tag-annotate` | `#1ba67320` | Background tint for annotation tag lines |

Sources: [packages/twoslash/styles/twoslash.css:2-11](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/styles/twoslash.css#L2-L11)

```css
/* ===== Error Line ===== */
.twoslash .twoslash-error-line {
  position: relative;
  background-color: var(--twoslash-error-bg);
  border-left: 3px solid var(--color-twoslash-error-foreground);
  color: var(--color-twoslash-error-foreground);
  padding: 6px 12px;
  margin: 0.2em 0;
}

.twoslash .twoslash-error {
  text-decoration: wavy underline var(--color-twoslash-error-foreground);
  padding-bottom: 2px;
}

```
Sources: [packages/twoslash/styles/twoslash.css:103-117](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/styles/twoslash.css#L103-L117)

Diagnostics lines use `.twoslash-error-line` to render a 3px solid left border matching `--color-twoslash-error-foreground`, paired with a background tint and padding. Inline errors (`.twoslash-error`) apply a wavy underline decoration using the error foreground color and a 2px bottom padding.

> [!WARNING]
> Error underlines depend on `text-decoration: wavy underline` tied directly to `--color-twoslash-error-foreground`; overriding text color without updating the error variable can cause low-contrast or invisible error markers in custom themes.

Sources: [packages/twoslash/styles/twoslash.css:104-116](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/styles/twoslash.css#L104-L116)

### Popup container layout and positioning

```css
.twoslash-completion-list,
.twoslash-popup-container {
  position: relative;
  user-select: none;
  display: flex;
  margin-top: 8px;
  width: 20rem;
  margin-right: -20rem;
  flex-direction: column;
  white-space: normal;
  box-shadow:
    0 4px 6px -1px rgb(0 0 0 / 0.1),
    0 2px 4px -2px rgb(0 0 0 / 0.1);
  border: 1px solid var(--color-fd-border);
  border-radius: 6px;
  padding: 8px;
  z-index: 8;
  color: var(--color-fd-popover-foreground);
  background-color: var(--color-fd-popover);
}
```
Sources: [packages/twoslash/styles/twoslash.css:17-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/styles/twoslash.css#L17-L36)

Popup containers and completion lists (`.twoslash-completion-list`, `.twoslash-popup-container`) are positioned relative with `user-select: none`, a fixed width of `20rem`, and a negative right margin of `-20rem` to prevent layout overflow. They incorporate box shadows, a 1px border linked to `--color-fd-border`, a border radius of 6px, and a high stacking index (`z-index: 8`).

```css
.twoslash-popup-arrow {
  position: absolute;
  top: -4px;
  left: 1em;
  transform: rotate(-45deg);
  pointer-events: none;
  width: 6px;
  height: 6px;
  border-top-width: 1px;
  border-right-width: 1px;
  border-color: var(--color-fd-primary);
  background-color: var(--color-fd-popover);
}

```
Sources: [packages/twoslash/styles/twoslash.css:47-60](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/styles/twoslash.css#L47-L60)

The popup arrow (`.twoslash-popup-arrow`) is positioned absolutely at `top: -4px` and `left: 1em` with a `-45deg` rotation. It renders a 6x6px square with top and right borders colored by `--color-fd-primary` and a background matching the popover surface.

```css
.fd-twoslash-popover {
  z-index: 50;
  min-width: 240px;
  max-width: min(450px, 96vw);
  max-height: 400px;
  overflow: auto;
  border-radius: var(--radius-xl);
  border: 1px solid var(--color-fd-border);
  background-color: var(--color-fd-popover);
  padding: calc(var(--spacing) * 3);
  font-size: var(--text-sm);
  color: var(--color-fd-popover-foreground);
  box-shadow: var(--shadow-md);
  transform-origin: var(--radix-popover-content-transform-origin);
}

.fd-twoslash-popover[data-state='open'] {
  animation: var(--animate-fd-popover-in);
}

.fd-twoslash-popover[data-state='closed'] {
  animation: var(--animate-fd-popover-out);
}

.fd-twoslash-popover:focus-visible {
  outline-style: none;
```
Sources: [packages/twoslash/styles/twoslash.css:199-224](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/styles/twoslash.css#L199-L224)

For Radix-integrated popovers (`.fd-twoslash-popover`), the stylesheet sets a higher stacking index (`z-index: 50`), a minimum width of 240px, a maximum width clamped to `min(450px, 96vw)`, and a maximum height of 400px with auto overflow. Open and closed animation states map directly to `--animate-fd-popover-in` and `--animate-fd-popover-out`.

Sources: [packages/twoslash/styles/twoslash.css:199-222](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/styles/twoslash.css#L199-L222)

## Related

- [Syntax Highlighting](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/syntax-highlighting)
- [TypeScript Type Table Extraction](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tooling/typescript-type-table-extraction)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
