Relevant source files
The following files were used as context for generating this wiki page:
- packages/core/src/mdx-plugins/rehype-code/shiki.ts
- packages/core/src/highlight/shiki/index.ts
- packages/core/src/mdx-plugins/rehype-code.core.ts
- packages/twoslash/src/index.ts
- packages/core/src/mdx-plugins/transformer-icon.ts
- packages/base-ui/css/lib/shiki.css
- packages/radix-ui/css/lib/shiki.css
- packages/core/src/highlight/shiki/full.ts
- packages/core/src/mdx-plugins/remark-code-tab.ts
- packages/mdx/src/loaders/mdx/remark-include.ts
- packages/core/src/mdx-plugins/rehype-code.ts
- packages/shadcn/src/manual-installation.ts
- packages/typescript/src/lib/remark-auto-type-table.ts
- packages/core/src/highlight/index.ts
- packages/core/src/highlight/shiki/react.ts
- packages/base-ui/src/components/codeblock.rsc.tsx
- packages/core/src/mdx-plugins/rehype-code/parsers.ts
- packages/mdx/src/webpack/meta.ts
- packages/core/src/mdx-plugins/codeblock-utils.ts
- packages/core/src/mdx-plugins/remark-llms.ts
- packages/radix-ui/src/components/dynamic-codeblock.core.tsx
- packages/typescript/src/markdown.ts
- packages/core/src/mdx-plugins/remark-structure.ts
- packages/core/src/highlight/client.ts
- packages/obsidian/src/mdx/index.ts
- packages/preview/src/components/markdown.tsx
- packages/base-ui/src/components/dynamic-codeblock.core.tsx
- packages/radix-ui/src/components/codeblock.rsc.tsx
- packages/core/src/mdx-plugins/remark-mdx-files.ts
- packages/core/src/mdx-plugins/stringifier.ts
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.
| 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
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').
ServerCodeBlockinvokeshighlightwith the code string and options.highlightretrieves or initializes the appropriate highlighter core viafactory.getOrInit().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 tofallbackLanguage(defaulting to'text').highlightHastresolves missing themes usingloadMissingThemeand loads the required language asynchronously usinghighlighter.loadLanguage(), before invokinghighlighter.codeToHastto 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 / 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 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, 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 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
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
| 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
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 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
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
Related
On This Page
OverviewUnified Rehype Shiki PipelineRehype AST Visitor and Element InterceptionHighlighter Core Delegation and CachingExtra Configuration Options ReferenceTheme Resolution and Highlighting EngineHighlighter Factory LifecycleTheme and Language LoadingAST Formatting and React Node RenderingCode Block Attribute ParsingPre Block Metadata and Inline Code ParsingMeta String Attribute ExtractionAST Transformers and Tab GroupingFile Icon Mapping and Shortcut ResolutionTab Metadata Extraction and Multi-Tab GroupingClient-Side Dynamic Highlighting HooksAsynchronous Shiki Execution and React HooksDynamic Code Block Component IntegrationCode Block Styling ArchitectureCSS Custom Properties and LayoutToken Color ContainmentInteractive Line States and ModifiersRelated