---
title: "Syntax Highlighting"
description: "Syntax Highlighting provides robust code block and inline syntax highlighting in Fumadocs by integrating Shiki into unified and remark-rehype processing pipelines. It intercepts code and pre elemen..."
last_updated: "2026-10-12T07:53:59.827853+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/syntax-highlighting"
---

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

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

- [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/core/src/highlight/shiki/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/index.ts)
- [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)
- [packages/twoslash/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts)
- [packages/core/src/mdx-plugins/transformer-icon.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/transformer-icon.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/full.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/full.ts)
- [packages/core/src/mdx-plugins/remark-code-tab.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-code-tab.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/core/src/mdx-plugins/rehype-code.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code.ts)
- [packages/shadcn/src/manual-installation.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/shadcn/src/manual-installation.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/core/src/highlight/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/index.ts)
- [packages/core/src/highlight/shiki/react.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/react.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/core/src/mdx-plugins/rehype-code/parsers.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/parsers.ts)
- [packages/mdx/src/webpack/meta.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/webpack/meta.ts)
- [packages/core/src/mdx-plugins/codeblock-utils.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/codeblock-utils.ts)
- [packages/core/src/mdx-plugins/remark-llms.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-llms.ts)
- [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/typescript/src/markdown.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/typescript/src/markdown.ts)
- [packages/core/src/mdx-plugins/remark-structure.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-structure.ts)
- [packages/core/src/highlight/client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/client.ts)
- [packages/obsidian/src/mdx/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/mdx/index.ts)
- [packages/preview/src/components/markdown.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/markdown.tsx)
- [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/radix-ui/src/components/codeblock.rsc.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/codeblock.rsc.tsx)
- [packages/core/src/mdx-plugins/remark-mdx-files.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-mdx-files.ts)
- [packages/core/src/mdx-plugins/stringifier.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/stringifier.ts)
</details>

## Overview

Syntax Highlighting provides robust code block and inline syntax highlighting in Fumadocs by integrating Shiki into unified and remark-rehype processing pipelines. It intercepts code and pre elements during AST traversal, parses meta string attributes such as line numbers and titles, applies transformers for diffing, focusing, and file icons, and supports asynchronous client-side highlighting via React hooks and runtime state management. Sources: [packages/core/src/mdx-plugins/rehype-code/shiki.ts:116-222](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/shiki.ts#L116-L222), [packages/core/src/mdx-plugins/rehype-code.core.ts:19-46](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code.core.ts#L19-L46), [packages/core/src/highlight/shiki/react.ts:27-59](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/react.ts#L27-L59)

## Unified Rehype Shiki Pipeline

### Rehype AST Visitor and Element Interception

The processor traverses the HAST tree using `unist-util-visit` to locate specific elements targeted for syntax highlighting. When the visitor encounters an element node matching `pre` or an inline `code` element where inline parsing is enabled, it delegates parsing to specialized parsers such as `PreParser` or `InlineCodeParsers`. Each candidate node must possess a valid parent and index to permit safe HAST node replacement.

```typescript
return async (tree) => {
  const queue: Promise<void>[] = [];

  visit(tree, 'element', (node, index, parent) => {
    let parsed: ShikiParsedData | undefined;

    // needed for hast node replacement
    if (!parent || index == null) return;

    if (node.tagName === 'pre') {
      parsed = PreParser(tree, node);
    } else if (node.tagName === 'code' && inline) {
      parsed = InlineCodeParsers[inline](tree, node);
```
Sources: [packages/core/src/mdx-plugins/rehype-code/shiki.ts:210-222](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/shiki.ts#L210-L222)

> [!WARNING]
> Nodes lacking a parent or index in the HAST tree are silently skipped during the AST traversal visitor callback. This safeguard prevents runtime reference errors when mutating the syntax tree structure.

Sources: [packages/core/src/mdx-plugins/rehype-code/shiki.ts:216-217](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/shiki.ts#L216-L217)

### Highlighter Core Delegation and Caching

Once a code element or pre block is intercepted, `onNode` extracts its metadata string, language identifier, and raw code content. If the designated language is not currently loaded in the core highlighter instance and lazy loading is active, it attempts to load the grammar dynamically from bundled languages or apply a fallback language. 

Before invoking the high-cost highlighter engine, the pipeline constructs a cache key incorporating the language, filtered meta string, and raw code. If a matching entry exists in the custom cache map, the cached HAST fragment is returned directly. Otherwise, transformers configured with post-enforcement extract asynchronous post-processing tasks, option mappings are assembled, trailing newlines are stripped when enabled, and `highlighter.codeToHast` is executed.

```typescript
async function onNode(
  tree: Root,
  node: Element,
  parsed: ShikiParsedData,
): Promise<Root | Element | undefined> {
  let { meta: metaString = '', lang = defaultLanguage, code } = parsed;
  if (!lang) return;

  if (!isLanguageLoaded(lang)) {
    if (lazy && lang in highlighter.getBundledLanguages()) {
      await highlighter.loadLanguage(lang as never);
    } else if (fallbackLanguage) {
      lang = fallbackLanguage;

      if (!isLanguageLoaded(fallbackLanguage))
        await highlighter.loadLanguage(fallbackLanguage as never);
    }
  }

  if (filterMetaString) {
    metaString = filterMetaString(metaString);
  }

  const cacheKey = `${lang}:${metaString}:${code}`;
  const cachedValue = cache?.get(cacheKey);

  if (cachedValue) {
    return cachedValue;
  }
  const transformers = rest.transformers ? [...rest.transformers] : [];
  let _fd_postprocess: ShikiTransformerContextMeta['_fd_postprocess'];
  transformers.push({
    enforce: 'post',
    root() {
      _fd_postprocess = this.meta._fd_postprocess;
    },
  });
  if (addLanguageClass) transformers.push(transformerAddLanguage(lang));

  const codeOptions: CodeToHastOptions = {
    ...rest,
    lang,
    structure: parsed.structure,
    transformers,
    meta: {
      ...rest.meta,
      __raw: metaString,
      ...parseMetaString?.(metaString, node, tree),
    },
  };

  if (stripEndNewline && code.endsWith('\n')) code = code.slice(0, -1);

  const fragment = highlighter.codeToHast(code, codeOptions);
  if (_fd_postprocess && _fd_postprocess.length > 0) {
    const ctx = { highlighter };
    await Promise.all(_fd_postprocess.map((v) => v(ctx)));
  }

  cache?.set(cacheKey, fragment);

  if (parsed.structure === 'classic') return fragment;
  return {
    type: 'element',
    tagName: 'code',
    properties: {
      class: 'shiki',
    },
    children: fragment.children as ElementContent[],
  };
}
```
Sources: [packages/core/src/mdx-plugins/rehype-code/shiki.ts:138-208](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/shiki.ts#L138-L208)

> [!NOTE]
> When `stripEndNewline` is set to `true`, trailing newline characters added by `mdast-util-to-hast` are automatically removed from the source string prior to passing it into Shiki's highlighter engine.

Sources: [packages/core/src/mdx-plugins/rehype-code/shiki.ts:54-61](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/shiki.ts#L54-L61), [packages/core/src/mdx-plugins/rehype-code/shiki.ts:189-189](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/shiki.ts#L189-L189)

### Extra Configuration Options Reference

The `RehypeShikiExtraOptions` interface governs behavior flags for language resolution, meta parsing, caching, and error handling during pipeline execution.

| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `addLanguageClass` | `boolean` | `false` | Add `language-*` class to code element. |
| `defaultLanguage` | `string` | `undefined` | The default language to use when not specified. |
| `fallbackLanguage` | `string` | `undefined` | The fallback language to use when specified language is not loaded or bundled. |
| `lazy` | `boolean` | `true` | Load languages and themes on-demand. |
| `stripEndNewline` | `boolean` | `true` | Strip trailing newline added by `mdast-util-to-hast`. |
| `parseMetaString` | `Function` | `undefined` | Custom meta string parser returning an object to merge with `meta`. |
| `filterMetaString` | `Function` | `undefined` | Filter meta string before processing. |
| `inline` | `InlineCodeParser \| false` | `false` | Highlight inline code blocks (e.g., `tailing-curly-colon`). |
| `cache` | `MapLike<string, Root>` | `undefined` | Custom map to cache transformed `codeToHast` results. |
| `onError` | `Function` | `undefined` | Error handler callback; throws if not provided. |

Sources: [packages/core/src/mdx-plugins/rehype-code/shiki.ts:28-101](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/shiki.ts#L28-L101)

## Theme Resolution and Highlighting Engine

### Highlighter Factory Lifecycle

The management and initialization of Shiki instances are coordinated through the `ShikiFactory` interface and `createShikiFactory` builder function. The factory pattern holds a cached instance or awaits initialization when `getOrInit` is called, ensuring that highlighter instances are instantiated on demand without redundant creation overhead. Two predefined factories are supplied: `defaultShikiFactory`, which provisions a standard highlighter instance utilizing the JavaScript regular expression engine, and `wasmShikiFactory`, which initializes a highlighter powered by the Oniguruma WebAssembly regex engine via `createOnigurumaEngine(import('shiki/wasm'))`. Both factories initialize with empty language and theme arrays, letting callers register grammars dynamically.

```typescript
/**
 * A factory for Fumadocs to manage Shiki instances.
 */
export interface ShikiFactory {
  init: (options?: InitOptions) => Awaitable<HighlighterCore>;
  getOrInit: () => Awaitable<HighlighterCore>;
}

export function createShikiFactory(config: Pick<ShikiFactory, 'init'>): ShikiFactory {
  let instance: Awaitable<HighlighterCore> | undefined;

  return {
    init(options) {
      return (instance = config.init(options));
    },
    getOrInit() {
      return instance ?? this.init();
    },
  };
}
```
Sources: [packages/core/src/highlight/shiki/index.ts:17-36](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/index.ts#L17-L36)

```typescript
export const defaultShikiFactory = createShikiFactory({
  async init(options) {
    const { createHighlighter, createJavaScriptRegexEngine } = await import('shiki');

    return createHighlighter({
      langs: [],
      themes: [],
      langAlias: options?.langAlias,
      engine: createJavaScriptRegexEngine(),
    });
  },
});

/** factory using the WASM powered Regex engine */
export const wasmShikiFactory = createShikiFactory({
  async init(options) {
    const { createHighlighter, createOnigurumaEngine } = await import('shiki');

    return createHighlighter({
      langs: [],
      themes: [],
      langAlias: options?.langAlias,
      engine: createOnigurumaEngine(import('shiki/wasm')),
    });
  },
```
Sources: [packages/core/src/highlight/shiki/full.ts:3-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/full.ts#L3-L27)

> [!NOTE]
> The `getOrInit` method returns the cached highlighter instance if available, falling back to invoking `init()` if the internal instance variable is undefined.

Sources: [packages/core/src/highlight/shiki/index.ts:32-34](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/index.ts#L32-L34)

### Theme and Language Loading

Theme resolution and grammar loading inspect incoming options against the active highlighter core. The high-level `highlight` and `highlightHast` functions accept `HighlightHastOptions`, which allow selecting the regex engine via the `engine` property (`'js'` or `'oniguruma'`). 

1. `ServerCodeBlock` invokes `highlight` with the code string and options.
2. `highlight` retrieves or initializes the appropriate highlighter core via `factory.getOrInit()`.
3. `highlightHast` (base implementation) resolves the target language, checking special language status and verifying if it is present in bundled or loaded languages; if missing, it falls back to `fallbackLanguage` (defaulting to `'text'`).
4. `highlightHast` resolves missing themes using `loadMissingTheme` and loads the required language asynchronously using `highlighter.loadLanguage()`, before invoking `highlighter.codeToHast` to produce the HAST root tree.

```typescript
export async function highlightHast(
  highlighter: HighlighterCore,
  code: string,
  options: HighlightHastOptions,
): Promise<Root> {
  const { fallbackLanguage = 'text', ...resolved } = options;
  const { isSpecialLang } = await import('shiki/core');
  if (
    !isSpecialLang(resolved.lang) &&
    !(resolved.lang in highlighter.getBundledLanguages()) &&
    !highlighter.getLoadedLanguages().includes(resolved.lang)
  ) {
    resolved.lang = fallbackLanguage;
  }

  await Promise.all([
    loadMissingTheme(highlighter, getRequiredThemes(resolved)),
    highlighter.loadLanguage(resolved.lang as never),
  ]);

  return highlighter.codeToHast(code, resolved);
```
Sources: [packages/core/src/highlight/shiki/index.ts:44-64](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/index.ts#L44-L64)

```typescript
export async function highlightHast(code: string, options: HighlightHastOptions): Promise<Root> {
  const engine = options.engine ?? 'js';
  const factory = engine === 'js' ? defaultShikiFactory : wasmShikiFactory;
  const instance = await factory.getOrInit();

  return highlightHastBase(instance, code, applyDefaultThemes(options));
}
```
Sources: [packages/core/src/highlight/index.ts:33-39](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/index.ts#L33-L39)

```mermaid
sequenceDiagram
    participant ServerCodeBlock as packages/radix-ui/src/components/codeblock.rsc.tsx
    participant highlight as packages/core/src/highlight/index.ts
    participant highlightHastBase as packages/core/src/highlight/shiki/index.ts
    ServerCodeBlock->>highlight: highlight(code, options)
    highlight->>highlightHastBase: highlightHast(instance, code, options)
    highlightHastBase->>highlightHastBase: highlightHast(highlighter, code, options)
```
Sources: [packages/radix-ui/src/components/codeblock.rsc.tsx:16-32](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/codeblock.rsc.tsx#L16-L32), [packages/core/src/highlight/index.ts:33-39](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/index.ts#L33-L39), [packages/core/src/highlight/shiki/index.ts:44-64](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/index.ts#L44-L64)

> [!CAUTION]
> If a requested language is neither a special language, present in bundled languages, nor loaded in the highlighter instance, it is silently overridden with the `fallbackLanguage`.

Sources: [packages/core/src/highlight/shiki/index.ts:51-57](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/index.ts#L51-L57)

### AST Formatting and React Node Rendering

Once `codeToHast` returns the HAST `Root` node, `highlight` transforms the syntax tree into React nodes using `toJsxRuntime`. The formatting pipeline integrates the JSX runtime elements with custom component overrides supplied via `options.components`, which map pre elements to specialized wrapper components such as `ServerCodeBlock`.

```typescript
export async function highlight(code: string, options: HighlightOptions): Promise<ReactNode> {
  return toJsxRuntime(await highlightHast(code, options), {
    ...JsxRuntime,
    development: false,
    components: options.components,
  });
```
Sources: [packages/core/src/highlight/index.ts:64-69](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/index.ts#L64-L69)

```tsx
export async function ServerCodeBlock({ code, codeblock, ...options }: ServerCodeBlockProps) {
  return await highlight(code, {
    defaultColor: false,
    ...options,
    components: {
      pre: (props) => (
        <CodeBlock
          {...props}
          {...codeblock}
          className={cn('my-0', props.className, codeblock?.className)}
        >
          <Pre>{props.children}</Pre>
        </CodeBlock>
      ),
      ...options.components,
    },
  });
```
Sources: [packages/radix-ui/src/components/codeblock.rsc.tsx:16-32](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/codeblock.rsc.tsx#L16-L32)

## Code Block Attribute Parsing

### Pre Block Metadata and Inline Code Parsing

The parsing pipeline inspects HAST nodes to extract code block metadata, language tags, and raw content from both classic fenced code blocks and inline code expressions. The `PreParser` function examines the first child element of a pre node, verifying that its tag name is `code` and that it possesses valid properties. It searches `head.properties.className` for any class string starting with `language-`, slicing off the prefix to determine the language identifier. Concurrently, it extracts metadata by checking `head.data?.meta` or falling back to `head.properties.metastring` as a string, defaulting to an empty string if neither exists.

```typescript
export const PreParser: ShikiParser = (_tree, node) => {
  const head = node.children[0];

  if (!head || head.type !== 'element' || head.tagName !== 'code' || !head.properties) {
    return;
  }

  const classes = head.properties.className;
  const languageClass = Array.isArray(classes)
    ? classes.find((d) => typeof d === 'string' && d.startsWith(languagePrefix))
    : undefined;

  return {
    structure: 'classic',
    lang:
      typeof languageClass === 'string' ? languageClass.slice(languagePrefix.length) : undefined,
    code: flattenNodeHast(head),
    meta: head.data?.meta ?? head.properties.metastring?.toString() ?? '',
  };
```
Sources: [packages/core/src/mdx-plugins/rehype-code/parsers.ts:33-51](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/parsers.ts#L33-L51)

For inline code elements, the `InlineCodeParsers` record provides the `'tailing-curly-colon'` parser. This parser flattens the HAST node into a raw string and matches it against the regular expression `RE_TAILING_CURLY_COLON` (`/(+)\{:([\w-]+)\}$/`). When a match occurs, it yields an inline structure type, extracts the code content from the first capture group, and retrieves the language identifier from the second capture group using `match.at(2)`.

```typescript
const RE_TAILING_CURLY_COLON = /(.+)\{:([\w-]+)\}$/;

export interface ShikiParsedData {
  meta?: string;
  lang?: string;
  code: string;
  structure: 'inline' | 'classic';
}

export type ShikiParser = (tree: Root, node: Element) => ShikiParsedData | undefined;

export type InlineCodeParser = 'tailing-curly-colon';

export const InlineCodeParsers: Record<InlineCodeParser, ShikiParser> = {
  'tailing-curly-colon': (_tree, node) => {
    const raw = flattenNodeHast(node);
    const match = raw.match(RE_TAILING_CURLY_COLON);
    if (!match) return;

    return {
      structure: 'inline',
      code: match[1] ?? raw,
      lang: match.at(2),
    };
  },
};
```
Sources: [packages/core/src/mdx-plugins/rehype-code/parsers.ts:4-29](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/parsers.ts#L4-L29)

> [!WARNING]
> If a pre element contains no children, or if its first child is not a code element, `PreParser` returns undefined without extracting any language or metadata.

Sources: [packages/core/src/mdx-plugins/rehype-code/parsers.ts:33-38](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/parsers.ts#L33-L38)

### Meta String Attribute Extraction

Fumadocs-style attributes embedded within code block meta strings—such as `title="hello world"`—are parsed via `parseCodeBlockAttributes`. This utility uses `AttributeRegex` (`/(?<=^|\s)(?<name>[a-zA-Z0-9_-]+)(?:=(?:"([^"]*)"|'([^']*)'))?/g`) to scan the meta string. It extracts attribute names alongside double-quoted or single-quoted string values, storing them in an attributes dictionary while stripping matched attributes from the remaining string output (`rest`). If `allowedNames` are specified, any attribute whose name is absent from the allowed list is ignored and left in the rest string.

```typescript
export interface CodeBlockAttributes<Name extends string = string> {
  attributes: Partial<Record<Name, string | null>>;
  rest: string;
}

const AttributeRegex = /(?<=^|\s)(?<name>[a-zA-Z0-9_-]+)(?:=(?:"([^"]*)"|'([^']*)'))?/g;

/**
 * Parse Fumadocs-style code block attributes from meta string, like `title="hello world"`
 */
export function parseCodeBlockAttributes<Name extends string = string>(
  meta: string,
  allowedNames?: Name[],
): CodeBlockAttributes<Name> {
  const attributes: CodeBlockAttributes['attributes'] = {};
  const rest = meta.replaceAll(AttributeRegex, (match, name, value_1, value_2) => {
    if (allowedNames && !allowedNames.includes(name)) return match;

    attributes[name] = value_1 ?? value_2 ?? null;
    return '';
  });

  return {
    rest,
    attributes,
  };
}
```
Sources: [packages/core/src/mdx-plugins/codeblock-utils.ts:89-115](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/codeblock-utils.ts#L89-L115)

| Function / Parser | Target Input | Return Structure | Key Behavior |
| :--- | :--- | :--- | :--- |
| `PreParser` | HAST `Element` (`pre`) | `ShikiParsedData` (`classic`) | Scans child code element classes for `language-` prefix and extracts `metastring` or `meta` data. |
| `InlineCodeParsers['tailing-curly-colon']` | HAST `Element` (`code`) | `ShikiParsedData` (`inline`) | Matches trailing `{:lang}` syntax to separate code content from inline language specifiers. |
| `parseCodeBlockAttributes` | Meta string (`string`) | `CodeBlockAttributes` | Parses key-value attributes via regex, optionally filtering against `allowedNames`, and returns unparsed remainder (`rest`). |

Sources: [packages/core/src/mdx-plugins/rehype-code/parsers.ts:6-51](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/rehype-code/parsers.ts#L6-L51), [packages/core/src/mdx-plugins/codeblock-utils.ts:89-115](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/codeblock-utils.ts#L89-L115)

## AST Transformers and Tab Grouping

### File Icon Mapping and Shortcut Resolution

Shiki code block decoration relies on language identifier translation and icon extraction. The transformer maps shorthand language aliases to normalized identifiers using `defaultShortcuts`. For example, `js` maps to `javascript`, `jsx` and `tsx` map to `react`, `ts` maps to `typescript`, `c#` and `cs` map to `csharp`, `gql` maps to `graphql`, `py` maps to `python`, and `bash`, `sh`, `shell`, and `zsh` all map to `shellscript`. C++ maps to `cpp`. 

```typescript
const defaultShortcuts: Record<string, string> = {
  js: 'javascript',
  jsx: 'react',
  ts: 'typescript',
  tsx: 'react',
  'c#': 'csharp',
  cs: 'csharp',
  gql: 'graphql',
  py: 'python',
  bash: 'shellscript',
  sh: 'shellscript',
  shell: 'shellscript',
  zsh: 'shellscript',
  'c++': 'cpp',
};
```
Sources: [packages/core/src/mdx-plugins/transformer-icon.ts:11-25](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/transformer-icon.ts#L11-L25)

The `CodeBlockIcon` type permits either an SVG path configuration object containing `viewBox`, `fill`, and `d` properties or a raw string identifier. Default icons sourced from Simple Icons are defined for languages including `react`, `shellscript`, `c`, `graphql`, `python`, `md`, `kotlin`, and `rust`.

```typescript
export type CodeBlockIcon =
  | {
      viewBox: string;
      fill: string;
      d: string;
    }
  | string;

const defaultShortcuts: Record<string, string> = {
  js: 'javascript',
  jsx: 'react',
  ts: 'typescript',
  tsx: 'react',
  'c#': 'csharp',
  cs: 'csharp',
  gql: 'graphql',
  py: 'python',
  bash: 'shellscript',
  sh: 'shellscript',
  shell: 'shellscript',
  zsh: 'shellscript',
  'c++': 'cpp',
};

// From https://simpleicons.org
// Java - Not supported, https://github.com/simple-icons/simple-icons/issues/7374
const defaultIcons: Record<string, CodeBlockIcon> = {
  react: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z',
  },
  vue: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'M24,1.61H14.06L12,5.16,9.94,1.61H0L12,22.39ZM12,14.08,5.16,2.23H9.59L12,6.41l2.41-4.18h4.43Z',
  },
  ruby: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'M20.156.083c3.033.525 3.893 2.598 3.829 4.77L24 4.822 22.635 22.71 4.89 23.926h.016C3.433 23.864.15 23.729 0 19.139l1.645-3 2.819 6.586.503 1.172 2.805-9.144-.03.007.016-.03 9.255 2.956-1.396-5.431-.99-3.9 8.82-.569-.615-.51L16.5 2.114 20.159.073l-.003.01zM0 19.089zM5.13 5.073c3.561-3.533 8.157-5.621 9.922-3.84 1.762 1.777-.105 6.105-3.673 9.636-3.563 3.532-8.103 5.734-9.864 3.957-1.766-1.777.045-6.217 3.612-9.75l.003-.003z',
  },
  zig: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'm23.53 1.02-7.686 3.45h-7.06l-2.98 3.452h7.173L.47 22.98l7.681-3.607h7.065v-.002l2.978-3.45-7.148-.001 12.482-14.9zM0 4.47v14.901h1.883l2.98-3.45H3.451v-8h.942l2.824-3.45H0zm22.117 0-2.98 3.608h1.412v7.844h-.942l-2.98 3.45H24V4.47h-1.883z',
  },
  swift: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'M7.508 0c-.287 0-.573 0-.86.002-.241.002-.483.003-.724.01-.132.003-.263.009-.395.015A9.154 9.154 0 0 0 4.348.15 5.492 5.492 0 0 0 2.85.645 5.04 5.04 0 0 0 .645 2.848c-.245.48-.4.972-.495 1.5-.093.52-.122 1.05-.136 1.576a35.2 35.2 0 0 0-.012.724C0 6.935 0 7.221 0 7.508v8.984c0 .287 0 .575.002.862.002.24.005.481.012.722.014.526.043 1.057.136 1.576.095.528.25 1.02.495 1.5a5.03 5.03 0 0 0 2.205 2.203c.48.244.97.4 1.498.495.52.093 1.05.124 1.576.138.241.007.483.009.724.01.287.002.573.002.86.002h8.984c.287 0 .573 0 .86-.002.241-.001.483-.003.724-.01a10.523 10.523 0 0 0 1.578-.138 5.322 5.322 0 0 0 1.498-.495 5.035 5.035 0 0 0 2.203-2.203c.245-.48.4-.972.495-1.5.093-.52.124-1.05.138-1.576.007-.241.009-.481.01-.722.002-.287.002-.575.002-.862V7.508c0-.287 0-.573-.002-.86a33.662 33.662 0 0 0-.01-.724 10.5 10.5 0 0 0-.138-1.576 5.328 5.328 0 0 0-.495-1.5A5.039 5.039 0 0 0 21.152.645 5.32 5.32 0 0 0 19.654.15a10.493 10.493 0 0 0-1.578-.138 34.98 34.98 0 0 0-.722-.01C17.067 0 16.779 0 16.492 0H7.508zm6.035 3.41c4.114 2.47 6.545 7.162 5.549 11.131-.024.093-.05.181-.076.272l.002.001c2.062 2.538 1.5 5.258 1.236 4.745-1.072-2.086-3.066-1.568-4.088-1.043a6.803 6.803 0 0 1-.281.158l-.02.012-.002.002c-2.115 1.123-4.957 1.205-7.812-.022a12.568 12.568 0 0 1-5.64-4.838c.649.48 1.35.902 2.097 1.252 3.019 1.414 6.051 1.311 8.197-.002C9.651 12.73 7.101 9.67 5.146 7.191a10.628 10.628 0 0 1-1.005-1.384c2.34 2.142 6.038 4.83 7.365 5.576C8.69 8.408 6.208 4.743 6.324 4.86c4.436 4.47 8.528 6.996 8.528 6.996.154.085.27.154.36.213.085-.215.16-.437.224-.668.708-2.588-.09-5.548-1.893-7.992z',
  },
  prisma: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'M21.8068 18.2848L13.5528.7565c-.207-.4382-.639-.7273-1.1286-.7541-.5023-.0293-.9523.213-1.2062.6253L2.266 15.1271c-.2773.4518-.2718 1.0091.0158 1.4555l4.3759 6.7786c.2608.4046.7127.6388 1.1823.6388.1332 0 .267-.0188.3987-.0577l12.7019-3.7568c.3891-.1151.7072-.3904.8737-.7553s.1633-.7828-.0075-1.1454zm-1.8481.7519L9.1814 22.2242c-.3292.0975-.6448-.1873-.5756-.5194l3.8501-18.4386c.072-.3448.5486-.3996.699-.0803l7.1288 15.138c.1344.2856-.019.6224-.325.7128z',
  },
  typescript: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z',
  },
  javascript: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.404-.601-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z',
  },
  php: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715l.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zM17.766 10.207h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z',
  },
  shellscript: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
    d: 'm 4,4 a 1,1 0 0 0 -0.7070312,0.2929687 1,1 0 0 0 0,1.4140625 L 8.5859375,11 3.2929688,16.292969 a 1,1 0 0 0 0,1.414062 1,1 0 0 0 1.4140624,0 l 5.9999998,-6 a 1.0001,1.0001 0 0 0 0,-1.414062 L 4.7070312,4.2929687 A 1,1 0 0 0 4,4 Z m 8,14 a 1,1 0 0 0 -1,1 1,1 0 0 0 1,1 h 8 a 1,1 0 0 0 1,-1 1,1 0 0 0 -1,-1 z',
  },
  c: {
    viewBox: '0 0 24 24',
    fill: 'currentColor',
```
Sources: [packages/core/src/mdx-plugins/transformer-icon.ts:3-115](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/transformer-icon.ts#L3-L115)

> [!NOTE]
> Java is not supported in the default icon set due to upstream limitations in Simple Icons.

Sources: [packages/core/src/mdx-plugins/transformer-icon.ts:27-28](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/transformer-icon.ts#L27-L28)

### Tab Metadata Extraction and Multi-Tab Grouping

The `remarkCodeTab` unified transformer scans Markdown AST nodes for contiguous code blocks carrying `tab` attributes in their meta strings. It processes these sequences via `parseCodeBlockAttributes`, extracting `tab` and `tab-group` metadata and populating `child.data.tab` and `child.data.tabGroup` properties.

```typescript
  if (child.type !== 'code' || !child.meta) {
    close();
    continue;
  }

  const meta = parseCodeBlockAttributes(child.meta, ['tab', 'tab-group']);
  if (!meta.attributes.tab) {
    close();
    continue;
  }

  if (start === -1) start = end;
  child.meta = meta.rest;
  child.data ??= {};
  child.data.tab = meta.attributes.tab;
  if (meta.attributes['tab-group']) {
    child.data.tabGroup = meta.attributes['tab-group'];
  }
}
```
Sources: [packages/core/src/mdx-plugins/remark-code-tab.ts:250-268](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-code-tab.ts#L250-L268)

The transformation delegates rendering to the `Types` handler record based on the configured `TabType`, which supports either `CodeBlockTabs` or `Tabs`. 

| Tab Handler Type | Target Node Structure | Persistence Support | MDX Parsing Support |
| :--- | :--- | :--- | :--- |
| `CodeBlockTabs` | `CodeBlockTabsOptions` via `generateCodeBlockTabs` | Optional `persist: { id: tagGroup }` derived from the first tab's `tabGroup` | Optional via `mdxToAst` processor when `parseMdx` is enabled |
| `Tabs` | `MdxJsxFlowElement` (`Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`) | None (relies on component props) | Full MDX element generation or simple attribute items mode |

Sources: [packages/core/src/mdx-plugins/remark-code-tab.ts:11-200](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-code-tab.ts#L11-L200)

> [!WARNING]
> When `Tabs` operates in simple mode without MDX parsing (`withMdx` set to false), it does not support rich MDX tab names and emits comma-joined literal arrays into the `items` attribute.

Sources: [packages/core/src/mdx-plugins/remark-code-tab.ts:92-146](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-code-tab.ts#L92-L146)

## Client-Side Dynamic Highlighting Hooks

### Asynchronous Shiki Execution and React Hooks

Browser-side syntax highlighting in Fumadocs relies on React state and suspense hooks that transform code strings into JSX nodes via Shiki and `hast-util-to-jsx-runtime`. The `useShikiDynamic` hook manages asynchronous highlighting tasks using `useEffect`, maintaining a task reference to prevent race conditions during rapid state changes. When a highlighting promise resolves, it checks whether `lastTask.current` matches the running promise before updating the node state.

```typescript
export function useShikiDynamic(
  highlighter: HighlighterCore | (() => Awaitable<HighlighterCore>),
  code: string,
  options: UseShikiOptions & { defaultValue?: ReactNode },
  deps: DependencyList,
): ReactNode {
  const [node, setNode] = useState(options.defaultValue);
  const lastTask = useRef<Promise<ReactNode> | null>(null);

  useEffect(() => {
    async function task() {
      const instance = typeof highlighter === 'function' ? await highlighter() : highlighter;

      return toJsxRuntime(await highlightHast(instance, code, options), {
        ...JsxRuntime,
        components: options.components,
      });
    }

    const promise = task();
    lastTask.current = promise;

    void promise.then((res) => {
      if (lastTask.current === promise) setNode(res);
    });
    return () => {
      lastTask.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);

  return node;
}
```
Sources: [packages/core/src/highlight/shiki/react.ts:27-59](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/react.ts#L27-L59)

The `useShiki` hook integrates with the React `use` API for suspenseful data fetching. It caches compiled results using JSON-serialized dependency arrays derived from language identifiers and code contents by default, or custom dependency lists when specified.

```typescript
export function useShiki(
  highlighter: HighlighterCore | (() => Awaitable<HighlighterCore>),
  code: string,
  options: UseShikiOptions,
  deps: DependencyList = [options.lang, code],
): ReactNode {
  const key = useMemo(() => JSON.stringify(deps), [deps]);

  async function run() {
    const instance = typeof highlighter === 'function' ? await highlighter() : highlighter;

    return toJsxRuntime(await highlightHast(instance, code, options), {
      ...JsxRuntime,
      components: options.components,
    });
  }

  return use((promises[key] ??= run()));
}
```
Sources: [packages/core/src/highlight/shiki/react.ts:66-84](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/react.ts#L66-L84)

A client-specific wrapper for `useShiki` initializes default themes automatically using `defaultShikiFactory.getOrInit()` combined with `applyDefaultThemes`.

```typescript
export function useShiki(
  code: string,
  options: HighlightOptions,
  deps?: DependencyList,
): ReactNode {
  return useShikiBase(
    () => defaultShikiFactory.getOrInit(),
    code,
    applyDefaultThemes(options),
    deps,
  );
}
```
Sources: [packages/core/src/highlight/client.ts:13-24](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/client.ts#L13-L24)

> [!NOTE]
> Results in `useShiki` are cached using `(lang, code)` pairs as keys unless an explicit dependency list overrides the memoization key.

Sources: [packages/core/src/highlight/shiki/react.ts:64-64](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/highlight/shiki/react.ts#L64-L64)

### Dynamic Code Block Component Integration

The `DynamicCodeBlock` component wraps dynamic client highlighting in a structured UI container. It accepts a highlighter instance or factory function, target language, raw code string, and optional configuration props.

```tsx
export function DynamicCodeBlock({
  lang,
  code,
  codeblock,
  options,
  wrapInSuspense = true,
  highlighter,
}: DynamicCodeblockProps) {
  const id = useId();
  const shikiOptions: UseShikiOptions = {
    lang,
    defaultColor: false,
    ...options,
    components: {
      pre: DefaultPre,
      ...options.components,
    },
  };
  let node = useShikiDynamic(highlighter, code, shikiOptions, [id, lang, code]);
  if (wrapInSuspense) node ??= <Placeholder code={code} components={shikiOptions.components} />;

  return <PropsContext value={codeblock}>{node}</PropsContext>;
}
```
Sources: [packages/radix-ui/src/components/dynamic-codeblock.core.tsx:45-67](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dynamic-codeblock.core.tsx#L45-L67)

| Property | Type | Default Value | Purpose |
| :--- | :--- | :--- | :--- |
| `highlighter` | `HighlighterCore \| (() => HighlighterCore \| PromiseLike<HighlighterCore>)` | *Required* | Shiki core instance or asynchronous initialization factory. |
| `lang` | `string` | *Required* | Target programming language identifier for syntax grammar selection. |
| `code` | `string` | *Required* | Raw source code text content to highlight. |
| `codeblock` | `CodeBlockProps` | `undefined` | Extra properties passed to the underlying `CodeBlock` component. |
| `wrapInSuspense` | `boolean` | `true` | Toggles automatic wrapping in a React `Suspense` boundary with a fallback placeholder. |
| `options` | `DistributiveOmit<UseShikiOptions, 'lang'>` | *Required* | Highlighting configuration options excluding language. |

Sources: [packages/radix-ui/src/components/dynamic-codeblock.core.tsx:8-25](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dynamic-codeblock.core.tsx#L8-L25)

When `wrapInSuspense` is enabled and the dynamic node has not yet resolved, a `Placeholder` component splits the raw code into individual line spans using `code.split('\n')` rendered inside `pre` and `code` elements.

```tsx
function Placeholder({
  code,
  components = {},
}: {
  code: string;
  components: UseShikiOptions['components'];
}) {
  const { pre: Pre = 'pre', code: Code = 'code' } = components as Record<string, FC>;

  return (
    <Pre>
      <Code>
        {code.split('\n').map((line, i) => (
          <span key={i} className="line">
            {line}
          </span>
        ))}
      </Code>
    </Pre>
  );
```
Sources: [packages/radix-ui/src/components/dynamic-codeblock.core.tsx:69-88](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dynamic-codeblock.core.tsx#L69-L88)

> [!WARNING]
> Defining a custom `pre` component inside `options.components` ignores extra `codeblock` properties passed to `DynamicCodeBlock`.

Sources: [packages/radix-ui/src/components/dynamic-codeblock.core.tsx:13-16](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/radix-ui/src/components/dynamic-codeblock.core.tsx#L13-L16)

## Code Block Styling Architecture

### CSS Custom Properties and Layout

Code blocks rely on CSS custom properties defined in package stylesheets to manage horizontal spacing and alignment. The padding variables `--padding-left` and `--padding-right` are calculated as four times the spacing scale token.

```css
.shiki:not(.not-fumadocs-codeblock *) {
  --padding-left: calc(var(--spacing) * 4);
  --padding-right: calc(var(--spacing) * 4);

```
Sources: [packages/base-ui/css/lib/shiki.css:1-4](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/css/lib/shiki.css#L1-L4)

Each line element within a Shiki block establishes a relative positioning context and applies these padding rules. Empty lines are explicitly sized using line height units to preserve vertical rhythm.

```css
.line {
  position: relative;
  padding-left: var(--padding-left);
  padding-right: var(--padding-right);
}

.line:empty {
  height: 1lh;
}
```
Sources: [packages/base-ui/css/lib/shiki.css:10-18](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/css/lib/shiki.css#L10-L18)

> [!NOTE]
> Empty lines set `height: 1lh` to ensure they maintain the exact height of populated text lines.

Sources: [packages/base-ui/css/lib/shiki.css:16-18](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/css/lib/shiki.css#L16-L18)

### Token Color Containment

Token coloring is scoped through CSS variables generated by Shiki for light and dark themes. Under standard selector rules, `code span` elements pull their color and font style properties directly from `--shiki-light` and `--shiki-light-font-style`.

```css
code span {
  color: var(--shiki-light);
  font-style: var(--shiki-light-font-style);
}
```
Sources: [packages/base-ui/css/lib/shiki.css:5-8](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/css/lib/shiki.css#L5-L8)

When the `.dark` class is present on an ancestor container, token colors override the light variables by binding to `--shiki-dark` and `--shiki-dark-font-style`. Inline code elements follow an identical scoping strategy via `code.shiki`.

```css
.dark .shiki:not(.not-fumadocs-codeblock *) {
  code span {
    color: var(--shiki-dark);
    font-style: var(--shiki-dark-font-style);
  }
}
```
Sources: [packages/base-ui/css/lib/shiki.css:92-97](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/css/lib/shiki.css#L92-L97)

### Interactive Line States and Modifiers

Code blocks support specialized line decorations including diff indicators, focus blur effects, line numbering, and custom word highlighting. When `.has-focused` is active on the container, unselected lines receive a 2px blur filter that transitions smoothly over 200 milliseconds unless hovered.

```css
&.has-focused .line:not(.focused) {
  filter: blur(2px);
  transition: filter 200ms;
}

&.has-focused:hover .line:not(.focused) {
  filter: blur(0);
}
```
Sources: [packages/base-ui/css/lib/shiki.css:20-27](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/css/lib/shiki.css#L20-L27)

| Class Selector | Applied Styles / Tailwind Mixins | Purpose |
| :--- | :--- | :--- |
| `.diff.remove` | `opacity: 0.7; bg-fd-diff-remove` | Styles removed lines in diff views with an opacity drop and background tint. |
| `.diff.add` | `bg-fd-diff-add` | Applies an addition background tint to newly introduced lines. |
| `.highlighted` | `bg-fd-primary/10` with primary border accent | Highlights specific target lines and renders an absolute left accent bar. |
| `.highlighted-word` | `border border-fd-primary/30 bg-fd-primary/10 rounded-md font-medium` | Outlines and backgrounds individual token words within code lines. |

Sources: [packages/base-ui/css/lib/shiki.css:34-69](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/css/lib/shiki.css#L34-L69)

When data line numbering is enabled (`[data-line-numbers]`), lines increment a CSS counter and prepend a pseudo-element displaying the current line index aligned to a fixed 7-spacing-unit column width.

```css
&[data-line-numbers] .line {
  counter-increment: line;
  padding-left: calc(var(--padding-left) + 7 * var(--spacing));
}

&[data-line-numbers] .line::after {
  position: absolute;
  content: counter(line);
  color: color-mix(
    in oklab,
    var(--fd-counter-color, var(--color-fd-muted-foreground)) 60%,
    transparent
  );
  @apply inset-y-0 left-0 w-7 text-right;
}
```
Sources: [packages/base-ui/css/lib/shiki.css:75-89](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/base-ui/css/lib/shiki.css#L75-L89)

## Related

- [Code Block Components](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/code-block-components)
- [Twoslash Type Hover](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/developer-tooling/twoslash-type-hover)


## Sitemap

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