Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Syntax Highlighting

Last updated Oct 12, 2026100% grounded
Relevant source files

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

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, packages/core/src/mdx-plugins/rehype-code.core.ts:19-46, packages/core/src/highlight/shiki/react.ts:27-59

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.

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

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

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.

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

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, packages/core/src/mdx-plugins/rehype-code/shiki.ts:189-189

Extra Configuration Options Reference

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

OptionTypeDefaultDescription
addLanguageClassbooleanfalseAdd language-* class to code element.
defaultLanguagestringundefinedThe default language to use when not specified.
fallbackLanguagestringundefinedThe fallback language to use when specified language is not loaded or bundled.
lazybooleantrueLoad languages and themes on-demand.
stripEndNewlinebooleantrueStrip trailing newline added by mdast-util-to-hast.
parseMetaStringFunctionundefinedCustom meta string parser returning an object to merge with meta.
filterMetaStringFunctionundefinedFilter meta string before processing.
inlineInlineCodeParser | falsefalseHighlight inline code blocks (e.g., tailing-curly-colon).
cacheMapLike<string, Root>undefinedCustom map to cache transformed codeToHast results.
onErrorFunctionundefinedError handler callback; throws if not provided.

Sources: packages/core/src/mdx-plugins/rehype-code/shiki.ts:28-101

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.

/**
 * 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

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

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

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.
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

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

Sources: packages/radix-ui/src/components/codeblock.rsc.tsx:16-32, packages/core/src/highlight/index.ts:33-39, packages/core/src/highlight/shiki/index.ts:44-64

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

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.

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

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

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.

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

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).

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

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

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.

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

Function / ParserTarget InputReturn StructureKey Behavior
PreParserHAST 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 syntax to separate code content from inline language specifiers.
parseCodeBlockAttributesMeta string (string)CodeBlockAttributesParses 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, packages/core/src/mdx-plugins/codeblock-utils.ts:89-115

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.

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

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.

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

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

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.

  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

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

Tab Handler TypeTarget Node StructurePersistence SupportMDX Parsing Support
CodeBlockTabsCodeBlockTabsOptions via generateCodeBlockTabsOptional persist: { id: tagGroup } derived from the first tab's tabGroupOptional via mdxToAst processor when parseMdx is enabled
TabsMdxJsxFlowElement (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

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

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.

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

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.

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

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

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

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

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.

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

PropertyTypeDefault ValuePurpose
highlighterHighlighterCore | (() => HighlighterCore | PromiseLike<HighlighterCore>)RequiredShiki core instance or asynchronous initialization factory.
langstringRequiredTarget programming language identifier for syntax grammar selection.
codestringRequiredRaw source code text content to highlight.
codeblockCodeBlockPropsundefinedExtra properties passed to the underlying CodeBlock component.
wrapInSuspensebooleantrueToggles automatic wrapping in a React Suspense boundary with a fallback placeholder.
optionsDistributiveOmit<UseShikiOptions, 'lang'>RequiredHighlighting configuration options excluding language.

Sources: packages/radix-ui/src/components/dynamic-codeblock.core.tsx:8-25

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.

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

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

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.

.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

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.

.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

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

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.

code span {
  color: var(--shiki-light);
  font-style: var(--shiki-light-font-style);
}

Sources: packages/base-ui/css/lib/shiki.css:5-8

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.

.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

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.

&.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

Class SelectorApplied Styles / Tailwind MixinsPurpose
.diff.removeopacity: 0.7; bg-fd-diff-removeStyles removed lines in diff views with an opacity drop and background tint.
.diff.addbg-fd-diff-addApplies an addition background tint to newly introduced lines.
.highlightedbg-fd-primary/10 with primary border accentHighlights specific target lines and renders an absolute left accent bar.
.highlighted-wordborder border-fd-primary/30 bg-fd-primary/10 rounded-md font-mediumOutlines and backgrounds individual token words within code lines.

Sources: packages/base-ui/css/lib/shiki.css:34-69

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.

&[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

Was this page helpful?