Relevant source files
The following files were used as context for generating this wiki page:
- packages/twoslash/src/index.ts
- packages/base-ui/css/lib/shiki.css
- packages/radix-ui/css/lib/shiki.css
- packages/core/src/highlight/shiki/index.ts
- packages/api-docs/src/components/schema/client.tsx
- packages/twoslash/package.json
- packages/twoslash/styles/twoslash.css
- packages/shadcn/src/rsc.tsx
- packages/typescript/src/markdown.ts
- packages/core/src/mdx-plugins/rehype-code/shiki.ts
- packages/typescript/src/ui/auto-type-table.tsx
- packages/twoslash/tsconfig.json
- packages/base-ui/src/components/dynamic-codeblock.core.tsx
- packages/core/src/highlight/shiki/react.ts
- packages/asyncapi/src/ui/index.tsx
- packages/core/src/highlight/shiki/full.ts
- packages/typescript/src/lib/remark-auto-type-table.ts
- packages/base-ui/src/components/codeblock.rsc.tsx
- packages/radix-ui/src/components/dynamic-codeblock.core.tsx
- packages/shadcn/src/manual-installation.ts
- packages/base-ui/src/components/dynamic-codeblock.tsx
- packages/openapi/src/ui/base.tsx
- packages/core/src/highlight/index.ts
- packages/core/src/highlight/client.ts
- packages/mdx/src/loaders/mdx/remark-include.ts
- packages/asyncapi/src/ui/components/codeblock.tsx
- packages/twoslash/src/ui/index.ts
- packages/radix-ui/src/components/codeblock.rsc.tsx
- packages/openapi/src/ui/components/codeblock.tsx
- packages/core/src/mdx-plugins/rehype-code.core.ts
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, packages/twoslash/package.json:2-4
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
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, packages/twoslash/tsconfig.json:1-8
Runtime and peer dependencies
The runtime package dependencies combine 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, packages/twoslash/package.json:66-73
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
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.
/**
* 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
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
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.
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
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
Markdown parsing in type hover
MDAST and HAST transformation pipelines
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
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.
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
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, packages/typescript/src/markdown.ts:52-53
Code block handling and Shiki error recovery
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
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.
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
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
Inline markdown and parameter tag formatting
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
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
'use client';
export * from './popup';Sources: packages/twoslash/src/ui/index.ts:1-2
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
Twoslash theme and layout styling
CSS variable themes and error decoration tokens
/* ===== 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
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
/* ===== 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
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
Popup container layout and positioning
.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
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).
.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
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.
.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
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
Related
On This Page
OverviewPackage architecture and dependenciesPublic entry points and file configurationRuntime and peer dependenciesShiki transformer integrationTransformer initialization and lazy compilationRenderer and query AST interceptionMarkdown parsing in type hoverMDAST and HAST transformation pipelinesCode block handling and Shiki error recoveryInline markdown and parameter tag formattingClient popup components and triggersClient entry points and popup re-exportsTwoslash theme and layout stylingCSS variable themes and error decoration tokensPopup container layout and positioningRelated