Warning
This is an internal project, and is not intended for public use. No support or stability guarantees are provided.
The useCode hook provides programmatic access to code display, editing, and transformation functionality within CodeHighlighter components. It's designed for scenarios where you need fine-grained control over code behavior or want to build custom code interfaces that focus purely on code management, without component rendering.
The hook implements the Props Context Layering pattern to work seamlessly across server and client boundaries, automatically merging initial props with enhanced context values.
The useCode hook orchestrates multiple specialized sub-hooks to provide a complete code management solution. It automatically integrates with CodeHighlighterContext when available, making it perfect for custom code interfaces that need to interact with the broader code highlighting system.
Key features include:
When a file has a focused source projection, its collapsed editor contains only that contiguous projection. Edits are patched into the corresponding range of the complete file before the controller receives the source. Padding included in the focused window is editable too. Files whose visible lines cannot be represented by one safe contiguous range use their complete source instead.
Expanding switches immediately to the complete source, without a height or source-swap animation. Collapsing restores the focused projection. Every loaded entry and extra file remains available in both states, including JavaScript, TypeScript, CSS, and CSS Modules.
Set resetOnExpand: true when expansion should discard all controlled source first. The
reset applies to every variant and file owned by the controller while preserving the
selected variant, file, and transform. It applies whether expansion comes from a toolbar,
keyboard boundary navigation, or a source deep link.
The editor wrapper is one normal tab stop. Press Enter to engage the textarea,
Escape to return to page navigation, and Tab to indent while editing. Pointer interaction engages the textarea directly. Read-only blocks do not load the editor.
Engaging an editable live demo activates its live runtime even before the first source change. Activation remains in effect until that demo unmounts; resetting source does not switch the preview back to its bundled component.
The hook exposes metadata for the selected file so controls do not need to inspect the rendered code tree:
selectedFileHasFocusProjection indicates that collapsed editing uses a focused projection.selectedFileCollapsible indicates that focused and complete views differ.selectedFileFocusedLines and selectedFileLines report focused and total line counts.selectedFileOriginalName is the canonical repository filename; selectedFileName is the displayed, potentially transformed filename.hasControlledEdits indicates whether the controller currently owns edited source.actionSource selects the source used by copy, copyMarkdown, and the export actions
inherited by useDemo:
'current' (default) uses controlled source, preserving the existing playground behavior.'initial' ignores controlled edits and uses repository source. The selected variant and
transform still apply, including transformed source and filenames.Use 'initial' when source actions should remain stable while the reader experiments:
const code = useCode(props, { actionSource: 'initial' });
Pass selectedTransform with onSelectedTransformChange to connect transform selection to
an external preference store. In controlled mode, URL selection still has initial precedence,
but internal local storage does not override the supplied value. Selecting a transform calls
the change handler; the displayed transform changes when the controlled value changes.
const code = useCode(props, {
selectedTransform: codeVariant === 'js' ? 'js' : null,
onSelectedTransformChange: (transform) => setCodeVariant(transform === 'js' ? 'js' : 'ts'),
});
Uncontrolled consumers can continue to use initialTransform and the stored preference.
Transform and variant changes retain the previous ready source while a cold transform loads, then commit once without an animated swap. Generic coordination and scroll-anchor hooks remain available for independent use.
The hook is built using a modular architecture with seven specialized sub-hooks:
import { useCode } from '@mui/internal-docs-infra/useCode';
import type { ContentProps } from '@mui/internal-docs-infra/CodeHighlighter/types';
function CodeContent(props: ContentProps<{}>) {
const code = useCode(props, {
initialVariant: 'TypeScript',
});
return (
<div>
<h3>{code.userProps.name}</h3>
<div>
Current: {code.selectedVariant}
<select
value={code.selectedVariant}
onChange={(event) => code.selectVariant(event.target.value)}
>
{code.variants.map((variant) => (
<option key={variant} value={variant}>
{variant}
</option>
))}
</select>
</div>
{/* File navigation with URL hash support */}
{code.files.length > 1 && (
<div>
{code.files.map((file) => (
<button
key={file.name}
onClick={() => code.selectFileName(file.name)}
className={code.selectedFileName === file.name ? 'active' : ''}
>
{file.name}
</button>
))}
</div>
)}
{code.selectedFile}
<button onClick={code.copy}>Copy Code</button>
</div>
);
}
When the code is rendered through a controller that supplies a setSource handler (see CodeControllerContext), the editor uses an explicit keyboard engagement step so normal page tab navigation is preserved. See Editable Code Interaction for the interaction details.
The live editor and asynchronous highlighting support are loaded on demand, so read-only code blocks never ship the editor. An editable block loads it once editing activates: an eager CodeProvider can bundle it for zero-latency editing, while CodeProviderLazy fetches it once per page. Until the editor resolves, the block stays selectable, read-only text.
Use the editActivation prop — set on CodeHighlighter or, more commonly, on a demo created with the demo factory — to defer the load until the reader actually engages the code:
'eager' (default) — load the editor as soon as the block is editable, and let CodeHighlighter speculatively preload it on first render.'interaction' — warm the editor on hover (pointerenter) and commit on focus or pointer down. The speculative preload is suppressed, so a block the reader never engages stays plain text and fetches nothing.// Editing activates only once the reader hovers, focuses, or clicks the code.
<CodeHighlighter editActivation="interaction" {...props} />
A related nicety — boundary auto-expand: in a collapsed collapsible block, the caret respects the collapsed-frame boundaries while editing, and moving past them (e.g. pressing ↓ on the last visible line) automatically expands the block instead of stranding the caret in hidden lines.
An editable block — one rendered through a controller that supplies setCode — is editable
from the start by default. To start it read-only and let the reader opt in, use the
editable / setEditable pair useCode returns to wire up a button:
function CodeBlock(props) {
const code = useCode(props);
return (
<div>
{code.setEditable && (
<button type="button" onClick={() => code.setEditable(!code.editable)}>
{code.editable ? 'View' : 'Edit'}
</button>
)}
{code.selectedFile}
</div>
);
}
editable — whether edit mode is on right now. While it's false the block stays
read-only and the live-editing engine isn't even warmed.setEditable(next) — flips edit mode. It is undefined when the block can't be
edited at all (no controller, or a hard disabled), so the button only renders where a
toggle is meaningful.editActivation
(the engine loads eagerly or on the next interaction); turning it off keeps the
reader's edits — use reset() to discard them.Start a block read-only with initialDisabled (a truthy value starts it read-only),
resolved exactly like initialExpanded — instance prop → demo meta → factory default:
createDemoFactory
call, to start every demo from that factory read-only:
const createDemo = createDemoFactory({ initialDisabled: true /* …other options */ });
createDemo call's meta (overrides the factory default; pass
false to opt a demo back in):
export const meta = { initialDisabled: true };
<Demo initialDisabled />.With none set, blocks stay editable. initialDisabled is distinct from disabled:
disabled turns editing off permanently (no toggle), while initialDisabled only sets
the initial state of a still-toggleable block.
| Parameter | Type | Description |
|---|---|---|
| contentProps | | |
| opts | |
UseCodeResult| Key | Type | Required |
|---|---|---|
| variants | | Yes |
| selectedVariant | | Yes |
| selectVariant | | Yes |
| files | | Yes |
| selectedFile | | Yes |
| selectedFileLines | | Yes |
| selectedFileFocusedLines | | Yes |
| selectedFileCollapsible | | Yes |
| selectedFileHasFocusProjection | | Yes |
| selectedFileOriginalName | | Yes |
| selectedFileName | | Yes |
| selectedFileUrl | | Yes |
| selectedFileSlug | | Yes |
| selectFileName | | Yes |
| allFilesSlugs | | Yes |
| expanded | | Yes |
| expand | | Yes |
| setExpanded | | Yes |
| copy | | Yes |
| copyMarkdown | | Yes |
| availableTransforms | | Yes |
| selectedTransform | | Yes |
| selectTransform | | Yes |
| hasControlledEdits | | Yes |
| editable | | Yes |
| setEditable | | No |
| setSource | | No |
| reset | | No |
| refresh | | No |
| userProps | | Yes |
type ActionSource = 'current' | 'initial'type CodeComponentsContext = React.Context<Partial<Components> | undefined>type UseCodeOpts = {
preClassName?: string;
copy?: UseCopierOpts;
githubUrlPrefix?: string;
initialVariant?: string;
initialTransform?: string;
/**
* Controls hash removal behavior when user interacts with file tabs:
* - 'remove-hash': Remove entire hash (default)
* - 'remove-filename': Remove only filename, keep variant in hash
*/
fileHashMode?: 'remove-hash' | 'remove-filename';
/**
* Controls when to save hash variant to localStorage:
* - 'on-load': Save immediately when page loads with hash
* - 'on-interaction': Save only when user clicks a tab (default)
* - 'never': Never save hash variant to localStorage
*/
saveHashVariantToLocalStorage?: 'on-load' | 'on-interaction' | 'never';
/**
* Array of enhancer functions to apply to parsed HAST sources.
* Enhancers receive the HAST root, comments extracted from source, and filename.
* Runs asynchronously when code changes.
*/
sourceEnhancers?: ((
root: { data?: unknown | undefined },
comments: {} | undefined,
fileName: string,
) => { data?: unknown | undefined } | Promise)[];
/** Disables editing of the code block even when a CodeControllerContext is present. */
disabled?: boolean;
/**
* Called when the code block is asked to expand its collapsed window — most
* importantly from the editor itself, when the caret navigates past the
* visible region (e.g. `ArrowUp` at the top of a collapsed block). Fires
* synchronously, *before* the expansion re-renders, so a host can capture the
* still-collapsed layout and engage a scroll anchor (e.g. `useCodeWindow`'s
* `anchorScroll('expand')`) — matching the timing of a click on the expand
* toggle. Without this, keyboard-driven expansion would jump the viewport
* instead of smoothly anchoring it.
*/
onExpand?: () => void;
/**
* Discards all controller-owned edits before a collapsed focused source is
* expanded. The selected variant, file, and transform are preserved.
*/
resetOnExpand?: boolean;
/**
* Selects whether copy and export actions use controlled source or the
* initial repository source. Defaults to `'current'`.
*/
actionSource?: ActionSource;
/**
* Controls the selected transform. When supplied, internal stored transform
* preferences do not override this value.
*/
selectedTransform?: string | null;
/** Called when a transform is selected in controlled mode. */
onSelectedTransformChange?: (transform: string | null) => void;
}type UseCodeResult<T extends {} = {}> = {
variants: string[];
selectedVariant: string;
selectVariant: (variant: string | null) => void;
files: { name: string; slug?: string; component: React.ReactNode }[];
selectedFile: React.ReactNode;
selectedFileLines: number;
/** Number of source lines shown by the collapsed editor. */
selectedFileFocusedLines: number;
/** Whether the selected file has distinct collapsed and complete views. */
selectedFileCollapsible: boolean;
/** Whether collapsed editing uses a contiguous focused projection. */
selectedFileHasFocusProjection: boolean;
/** Canonical repository filename before a transform renames the display. */
selectedFileOriginalName: string | undefined;
selectedFileName: string | undefined;
/**
* URL of the currently selected file, derived from the selected variant's
* `url`, the file's name, and its `relativeUrl` (when set). `undefined` when
* the variant has no `url` or the URL cannot be resolved.
*/
selectedFileUrl: string | undefined;
/**
* Slug for the currently selected file. Always derived from the canonical
* (original) file name — transforms are a view preference and do not
* produce separate slugs. Useful for building permalinks (e.g. `#${slug}`)
* that survive transform changes.
*/
selectedFileSlug: string | undefined;
selectFileName: (fileName: string) => void;
allFilesSlugs: { fileName: string; slug: string; variantName: string }[];
expanded: boolean;
expand: () => void;
setExpanded: (expanded: boolean) => void;
copy: (event: React.MouseEvent) => Promise<void>;
/**
* Copies all files in the current variant to the clipboard as a Markdown
* snippet (heading + per-file fenced code blocks).
*/
copyMarkdown: (event: React.MouseEvent) => Promise<void>;
availableTransforms: string[];
selectedTransform: string | null | undefined;
selectTransform: (transformName: string | null) => void;
/** Whether the surrounding controller currently owns edited source. */
hasControlledEdits: boolean;
/**
* Whether edit mode is currently on. `true` by default; starts `false` when the block
* opts in via `initialDisabled` — per demo through the `createDemo` `meta`, or across a
* factory's demos through `createDemoFactory`. While `false` the block renders
* read-only and the live-editing engine is not even warmed; flip it with
* `setEditable`. Independent of `editActivation`, which governs *when* the engine
* loads once editing is on, and of `disabled`, which turns editing off permanently.
*/
editable: boolean;
/**
* Turns edit mode on or off. `undefined` when the block can't be edited at all — no
* `CodeControllerContext` with `setCode` in scope, or editing is hard-`disabled` — so
* a toggle button can render only when this is defined. Toggling off keeps the
* reader's edits (use `reset` to discard them).
*/
setEditable?: (editable: boolean) => void;
/**
* Replace the source of the currently selected file (or `fileName` when
* provided) in the controlled code. Internal hooks may pass additional
* arguments (caret position, pre-parsed HAST) that are not part of the
* public contract.
*/
setSource?: (source: string, fileName?: string) => void;
/**
* Clears the entire controlled code state back to `undefined`, discarding
* user edits across **all variants and files** owned by the surrounding
* `CodeControllerContext` (not just the currently selected file or
* variant). Only available when a `CodeControllerContext` with `setCode`
* is in scope and editing is not disabled.
*/
reset?: () => void;
/**
* Re-fetches the block's data on the client by re-running the full variant
* loader, then swaps in the fresh result while keeping the current highlighted
* output visible until the new tree lands (stale-while-revalidate). Invalidates
* the pre-parsed HAST cache. `undefined` (or a no-op) for a block with no `url`
* to re-fetch from, or with no `CodeProvider` in scope.
*/
refresh?: () => void;
userProps: UserProps<T>;
}useCode (and useDemo) return a refresh() callback for re-fetching a block's
source on the client — useful when the underlying code can change (live data, a
just-saved file, a "reload" control):
const { refresh, files, selectedFile } = useCode(contentProps);
// e.g. a reload button
<button type="button" onClick={() => refresh()}>
Reload
</button>refresh() re-runs the full variant loader (loadIsomorphicCodeVariant),
not the stripped initial-fallback loader, so every variant, transform, and extra
file comes back fresh. It is stale-while-revalidate: the current highlighted
output stays on screen until the new tree is ready (via the same deferred-highlight
gate used for the first load), so there is no flash of unhighlighted or empty
content. The per-file pre-parsed HAST cache is invalidated so the refreshed source
re-highlights rather than reusing stale spans.
It is a no-op when there is nothing to re-fetch — a block with no url, or one
rendered without a CodeProvider. On
the server refresh is undefined; it only does work on the client.
useCode returns two clipboard callbacks:
copy — copies the selected file's plain text.copyMarkdown — copies the entire variant as a Markdown snippet: the
block's name as a heading, then one fenced code block per file with the
filename rendered as a comment in each language's syntax. Ready to paste into
an issue, a chat message, or an LLM prompt.function CopyControls(props) {
const code = useCode(props);
return (
<div>
<button onClick={code.copy}>Copy file</button>
<button onClick={code.copyMarkdown}>Copy as Markdown</button>
{code.selectedFile}
</div>
);
}
Both are wired through useCopier; pass the copy
option (UseCopierOpts) to configure success-state timing and callbacks for
both. Compressed sources are decoded back to text on demand, so neither copy
forces an early decompression of the block.
Note
For lists and standalone chunks, the lower-level
CoordinatedLazy/useStreamprimitives expose the samerefresh()plus an opt-inrevalidateOnIdlethat automatically revalidates once the browser goes idle after the initial load.
The useCode hook automatically manages URL hashes to reflect the currently selected file. This provides deep-linking capabilities and preserves navigation state across page reloads.
function CodeViewer(props) {
const code = useCode(props, {
initialVariant: 'TypeScript',
});
// File selection automatically updates URL hash without polluting browser history
// URLs follow pattern: #mainSlug:fileName or #mainSlug:variant:fileName
return (
<div>
{code.files.map((file) => (
<button
key={file.name}
onClick={() => code.selectFileName(file.name)}
className={code.selectedFileName === file.name ? 'active' : ''}
>
{file.name}
</button>
))}
{/* File slug can be used for sharing or bookmarking */}
<span>
Current file URL: #{code.files.find((f) => f.name === code.selectedFileName)?.slug}
</span>
{code.selectedFile}
</div>
);
}
The hook provides fine-grained control over URL hash management through two complementary options:
Controls what happens to the URL hash when users interact with file tabs or switch variants.
Options:
'remove-hash' (default): Complete hash removal on interaction
'remove-filename': Keep variant in hash on interaction
#demo:variant:file.tsx → #demo:variant)#demo:file.tsx → #demo)Controls when a hash-specified variant gets saved to localStorage for future visits.
Options:
'on-load': Save immediately when page loads with hash
'on-interaction' (default): Save only when user interacts
'never': Never save hash variant to localStorage
Default behavior - Remove hash on interaction:
function StandardCodeViewer(props) {
const code = useCode(props, {
fileHashMode: 'remove-hash', // Can be omitted as it's the default
saveHashVariantToLocalStorage: 'on-interaction', // Can be omitted as it's the default
});
// User visits #demo:variant:file.tsx - that file is selected
// User clicks another file tab - hash is removed completely
// Variant is saved to localStorage (will be default for future visits)
}
Keep variant in URL after interaction:
function VariantAwareCodeViewer(props) {
const code = useCode(props, {
fileHashMode: 'remove-filename',
});
// User visits #demo:typescript:file.tsx - that file in TypeScript variant is selected
// User clicks another file tab - hash becomes #demo:typescript
// Variant remains visible in URL for easy sharing
// Variant is saved to localStorage (will be default for future visits)
}
Hash navigation without affecting preferences:
function ShareableCodeViewer(props) {
const code = useCode(props, {
fileHashMode: 'remove-hash',
saveHashVariantToLocalStorage: 'never',
});
// User visits #demo:variant:file.tsx - that file is selected
// User clicks another file tab - hash is removed
// Variant is NOT saved - next visit uses their preferred variant from localStorage
// Perfect for shareable links that don't override personal preferences
}
Persistent variant preference from first visit:
function StickyVariantCodeViewer(props) {
const code = useCode(props, {
fileHashMode: 'remove-filename',
saveHashVariantToLocalStorage: 'on-load',
});
// User visits #demo:tailwind:file.tsx - Tailwind variant is immediately saved
// Future visits default to Tailwind variant even without hash
// Hash is simplified to #demo:tailwind when user clicks tabs
// Perfect for documentation where first variant choice should stick
}
When name or slug properties are not provided, the hook automatically generates them from the url property (or context URL). This is particularly useful when working with file-based demos or dynamic content.
// Component with explicit name and slug
<CodeHighlighter
name="Custom Button"
slug="custom-button"
Content={MyCodeContent}
>
{/* code */}
</CodeHighlighter>
// Component with automatic generation from URL
<CodeHighlighter
url="file:///app/components/demos/advanced-table/index.ts"
Content={MyCodeContent}
>
{/*
Automatically generates:
- name: "Advanced Table"
- slug: "advanced-table"
*/}
</CodeHighlighter>
function MyCodeContent(props) {
const code = useCode(props);
// Access generated user properties
console.log(code.userProps.name); // "Advanced Table"
console.log(code.userProps.slug); // "advanced-table"
return <div>{/* render code */}</div>;
}
function TransformSelector(props) {
const code = useCode(props, {
initialTransform: 'typescript',
});
return (
<div>
{code.availableTransforms.length > 0 && (
<select
value={code.selectedTransform || ''}
onChange={(event) => code.selectTransform(event.target.value || null)}
>
<option value="">No Transform</option>
{code.availableTransforms.map((transform) => (
<option key={transform} value={transform}>
{transform}
</option>
))}
</select>
)}
{code.selectedFile}
</div>
);
}
The hook automatically integrates with CodeHighlighterContext when used as a Content component:
// Simple wrapper component using CodeHighlighter directly
export function Code({ children, fileName }: { children: string; fileName?: string }) {
return (
<CodeHighlighter
fileName={fileName}
Content={CodeContent} // Your custom content component using useCode
sourceParser={createParseSource()}
>
{children}
</CodeHighlighter>
);
}
// Your custom content component using useCode
function CodeContent(props: ContentProps<{}>) {
// Automatically receives code from CodeHighlighter context
const code = useCode(props);
return (
<div>
{code.selectedFile}
<button onClick={code.copy}>Copy</button>
</div>
);
}
// Usage - simple and direct
<Code fileName="example.ts">
{`function hello() {
console.log('Hello, world!');
}`}
</Code>When your code has multiple files, you should provide navigation between them. The recommended approach is to use conditional display - only show tabs when multiple files exist, otherwise show just the filename:
Note
If you're creating demos that combine component previews with multi-file code examples, consider using
useDemoinstead, which handles both component rendering and file navigation.
Tip
Progressive Enhancement: Use
<a>tags withhrefattributes for file tabs to enable navigation before JavaScript loads. TheselectFileNamefunction will prevent default navigation after hydration, while CSS:targetand:has()selectors can show/hide content based on the URL hash.If a user clicks a tab before JavaScript loads, the browser navigates to the hash URL. When the page hydrates, the hook reads this hash and updates the React state accordingly - no clicks are lost during the transition from CSS-only to JavaScript-controlled navigation.
function CodeWithTabs(props) {
const code = useCode(props, { preClassName: styles.codeBlock });
return (
<div className={styles.container}>
<div className={styles.header}>
{code.files.length > 1 ? (
<div className={styles.tabContainer}>
{code.files.map((file) => (
<a
key={file.name}
href={`#${file.slug}`}
onClick={(event) => {
event.preventDefault();
code.selectFileName(file.name);
}}
className={`${styles.tab} ${
code.selectedFileName === file.name ? styles.active : ''
}`}
>
{file.name}
</a>
))}
</div>
) : (
<span className={styles.fileName}>{code.selectedFileName}</span>
)}
</div>
<div className={styles.codeContent}>
{code.files.map((file) => (
<div
key={file.name}
id={file.slug}
className={`${styles.codeFile} ${
code.selectedFileName === file.name ? styles.selected : ''
}`}
>
{file.component}
</div>
))}
</div>
</div>
);
}
CSS for Progressive Enhancement:
/* Hide all files by default */
.codeFile {
display: none;
}
/* Show first file when no hash target exists */
.codeContent:not(:has(.codeFile:target)) .codeFile:first-of-type {
display: block;
}
/* Show targeted file when hash exists */
.codeFile:target {
display: block;
}
/* After JS loads, let React control visibility */
.container:has(.tab.active) .codeFile {
display: none; /* Disable CSS-based visibility when JS is active */
}
.container:has(.tab.active) .codeFile.selected {
display: block; /* React-controlled selection */
}
This pattern ensures a clean user experience by avoiding unnecessary tab UI when only one file exists.
The hook generates URL hashes for file navigation following these patterns:
When the variant name is "Default", it's omitted from the hash:
#mainSlug:fileName
# Examples:
#button-demo:button.tsx
#data-table:index.ts
#advanced-form:styles.css
All other variants include the variant name in the hash:
#mainSlug:variantName:fileName
# Examples:
#button-demo:tailwind:button.tsx
#data-table:typescript:index.ts
#advanced-form:styled-components:styles.css
Variants (except "Default") can also be referenced without a filename:
#mainSlug:variantName
# Examples:
#button-demo:tailwind
#data-table:typescript
#advanced-form:styled-components
These variant-only hashes select the main file of that variant.
ButtonWithTooltip.tsx become button-with-tooltip.tsxInitial Load:
initialVariant > first variantUser Interactions:
fileHashMode option
'remove-hash': Removes entire hash'remove-filename': Keeps variant in hash (e.g., #demo:variant)fileHashMode option
'remove-hash': Removes entire hash'remove-filename': Updates hash to reflect new variantlocalStorage Persistence:
saveHashVariantToLocalStorage option'on-load': Hash variant saved immediately when page loads'on-interaction': Hash variant saved only when user clicks a file tab'never': Hash variant never saved to localStorageAuto-Expansion:
The useCode hook is composed of several specialized sub-hooks that can be used independently:
Manages variant selection logic and provides variant-related data. Implements a priority system for variant selection:
The hook parses variants from URL hashes and optionally persists them to localStorage based on the saveHashVariantToLocalStorage configuration.
Handles code transforms, including delta validation and transform application.
Manages file selection and navigation within code variants. Features:
fileHashModeselectedFileUrl by resolving the selected file's name (or its relativeUrl) against the variant's url. Available on the useCode result so consumers can link back to the source file (e.g., for an "Edit on GitHub" link).The hook generates slugs for all files across all variants, with special handling for the "Default" variant (which omits the variant name from the hash).
Controls UI-related state like expansion management. Automatically expands code demos when a relevant URL hash is present, ensuring hash-linked content is immediately visible to users.
Handles clipboard operations and copy state management.
Manages source code editing capabilities when available.
Applies enhancer functions to parsed HAST sources asynchronously. Enhancers run in order after parsing and can modify the syntax tree (e.g., adding annotations, highlighting specific lines, injecting comments).
Source enhancers allow you to modify the parsed HAST (Hypertext Abstract Syntax Tree) before rendering. This is useful for adding custom annotations, highlighting specific code patterns, or injecting metadata into the code display.
Enhancers can be passed to either:
CodeHighlighter component via the sourceEnhancers prop (runs on server or during hydration)useCode hook via the sourceEnhancers option (runs on client after hydration)import type { SourceEnhancer, HastRoot } from '@mui/internal-docs-infra/CodeHighlighter/types';
// Simple enhancer that adds data attributes to line elements
const addLineAnnotations: SourceEnhancer = (root, comments, fileName) => {
// Traverse and modify HAST nodes...
return root;
};
// Pass to CodeHighlighter (server-side or during hydration)
<CodeHighlighter sourceEnhancers={[addLineAnnotations]} Content={MyCodeContent}>
{code}
</CodeHighlighter>;
// Or pass to useCode (client-side, after hydration)
function MyCodeContent(props) {
const enhancers = React.useMemo(() => [addLineAnnotations], []);
const code = useCode(props, { sourceEnhancers: enhancers });
return <div>{code.selectedFile}</div>;
}
Enhancers can capture state through closures, allowing dynamic behavior based on user interactions or application state:
function CodeWithDynamicHighlighting(props) {
const [highlightedLines, setHighlightedLines] = React.useState<number[]>([]);
// Create enhancer that captures current state
const enhancers = React.useMemo(() => {
const lineHighlighter: SourceEnhancer = (root, comments, fileName) => {
// Use highlightedLines from closure to add data-hl attributes
return addHighlightToLines(root, highlightedLines);
};
return [lineHighlighter];
}, [highlightedLines]); // Re-create when state changes
const code = useCode(props, { sourceEnhancers: enhancers });
return (
<div>
<button onClick={() => setHighlightedLines([1, 2, 3])}>Highlight first 3 lines</button>
{code.selectedFile}
</div>
);
}
Tip
When passing state to enhancers, include the state values in the
useMemodependency array. This ensures the enhancer is re-created when state changes, triggering re-enhancement of the code.
Enhancers can be asynchronous, useful for fetching additional metadata or performing complex transformations:
const asyncEnhancer: SourceEnhancer = async (root, comments, fileName) => {
// Fetch metadata or perform async operations
const metadata = await fetchCodeMetadata(fileName);
// Return enhanced HAST
return addMetadataToHast(root, metadata);
};
Multiple enhancers run in sequence, each receiving the output of the previous:
const enhancers = React.useMemo(
() => [highlightDeprecatedCode, addLineNumbers, injectTypeAnnotations],
[],
);
const code = useCode(props, { sourceEnhancers: enhancers });
Enhancers receive parsed comments from the source code, enabling comment-driven enhancements:
const commentHighlighter: SourceEnhancer = (root, comments, fileName) => {
if (!comments) return root;
// Use comments to add highlighting or annotations
// comments structure: Record<number, string[]>
// where key is the 1-indexed line number, value is array of comments on that line
return processCommentsIntoHighlights(root, comments);
};
For optimal performance, enhancers should run during build or server rendering whenever possible. This allows results to be cached and eliminates client-side processing overhead.
You can combine enhancers at different stages—they apply in sequence:
// Build-time enhancer (configured in webpack loader or createDemo factory)
// Results are cached and included in the precomputed output
const buildTimeEnhancer: SourceEnhancer = (root, comments) => {
return addLineHighlightsFromComments(root, comments);
};
// Server-time enhancer (passed to CodeHighlighter in a Server Component)
// Runs on each request, useful for dynamic server data
const serverEnhancer: SourceEnhancer = (root, comments, fileName) => {
return addServerSideAnnotations(root, serverData);
};
// In a Server Component:
<CodeHighlighter sourceEnhancers={[serverEnhancer]} Content={MyCodeContent}>
{code}
</CodeHighlighter>;
// Client-time enhancer (applied via useCode options)
// Runs on hydration, useful for dynamic behavior
const clientEnhancer: SourceEnhancer = (root, comments, fileName) => {
return addPersonalizedAnnotations(root, userPreferences);
};
function MyCodeContent(props) {
const enhancers = React.useMemo(() => [clientEnhancer], []);
const code = useCode(props, { sourceEnhancers: enhancers });
return <div>{code.selectedFile}</div>;
}
When to use build-time enhancers:
// @highlight)When to use server-time enhancers:
When to use client-time enhancers:
Note
The Precompute Loader only supports configurable built-in enhancers—you cannot pass custom enhancer functions directly. For custom enhancer logic at build/server time, pass
sourceEnhancersto theCodeHighlightercomponent in a Server Component. Client-time enhancers are passed throughuseCodeoptions.
Enhancers and transformers serve different purposes and operate at different stages:
| Aspect | Enhancers | Transformers |
|---|---|---|
| Purpose | Add visual annotations to HAST | Modify the source code itself |
| Operates on | HAST (parsed syntax tree) | Plain text source code |
| Stage | After parsing, in CodeHighlighter or useCode | Before parsing, in CodeHighlighter |
| Plain text impact | Must NOT change plain text output | Can change plain text output |
Caution
Enhancers must not change the plain text output. They should only add wrapper HTML elements, add/remove properties and class names, or inject metadata—never modify or remove text nodes.
Changing the text content would cause layout shift when the code transitions from plain text (shown during initial render/loading) to the fully highlighted and enhanced version.
This constraint is why comments extracted with notableCommentsPrefix are removed from the source before the plain text version is generated. If comment removal happened in an enhancer instead, the line numbers would shift and cause a jarring visual jump when the enhanced code replaces the plain text placeholder.
Use enhancers for:
data-* attributes (e.g., data-hl for line highlighting)<span> elementsUse transformers for:
Note
Transformers are for alternatives, not modifications. Transformers create additional views of the code that users can toggle between—they should not be used to alter the default version. If you need to modify the source itself (e.g., fixing formatting, removing debug code), do that in a linter or build step before the code reaches
CodeHighlighter.
See the Code Highlighter transforms documentation for details on using transformers.
Warning
Stable References Required: Always use
React.useMemoor define enhancer arrays outside your component to prevent infinite re-render loops. Creating new arrays on each render will cause the hook to continuously re-run enhancement.
Note
HAST Sources Only: Enhancers only run on parsed HAST content (pre-computed at build time or parsed at runtime). String sources that haven't been parsed will not trigger enhancement.
// Recommended: Let the hook generate name/slug from URL
<CodeHighlighter
url="file:///components/demos/advanced-search/index.ts"
Content={CodeContent}
>
{/* Automatically gets name: "Advanced Search", slug: "advanced-search" */}
</CodeHighlighter>
// Override only when needed
<CodeHighlighter
url="file:///components/demos/search/index.ts"
name="Custom Search Component" // Override auto-generated name
Content={CodeContent}
>
{/* Uses custom name, but auto-generated slug: "search" */}
</CodeHighlighter>
function CodeViewer(props) {
const code = useCode(props, {
initialVariant: 'TypeScript', // Fallback if no URL hash
});
// URL hash automatically handled - no manual intervention needed
// Users can bookmark specific files and return to them
return (
<div>
{code.files.map((file) => (
<button
key={file.name}
onClick={() => code.selectFileName(file.name)}
data-slug={file.slug} // Available for analytics or debugging
>
{file.name}
</button>
))}
{code.selectedFile}
</div>
);
}
// Recommended: Use for code-specific functionality
function CodeSelector(props) {
const code = useCode(props);
return (
<div>
<VariantSelector
variants={code.variants}
selected={code.selectedVariant}
onSelect={code.selectVariant}
/>
{code.selectedFile}
</div>
);
}
function SafeCodeInterface(props) {
const code = useCode(props);
if (!code.selectedFile) {
return <div>Loading code…</div>;
}
return <div>{code.selectedFile}</div>;
}
const code = useCode(props, {
initialVariant: 'TypeScript', // Pre-select variant
initialTransform: 'js', // Pre-apply transform
});
Whether the block starts expanded is not a useCode option — it's
initialExpanded, set on contentProps (via <CodeHighlighter initialExpanded />,
createDemo(..., { initialExpanded: true }), or <Demo initialExpanded />). It
lives there rather than in useCode opts so the loading fallback honors it too.
The hook includes built-in error handling for:
Errors are logged to the console and the hook gracefully degrades functionality when errors occur.
Transforms are only shown when they have meaningful changes. Empty transforms are automatically filtered out.
Ensure your component is used within a CodeHighlighter component that provides the necessary context.
code.files[].slug values for debuggingurl property is provided to CodeHighlighter or available in contextname and slug properties as fallbacksextraFiles and main files don't have conflicting namesfileHashMode is set to 'remove-hash' (default) or 'remove-filename'fileHashMode: 'remove-filename' to preserve variant in hashmainSlug:fileNamemainSlug:variant:fileName or mainSlug:variantsaveHashVariantToLocalStorage setting:
'on-load': Saves immediately when page loads with hash'on-interaction': Saves only when user clicks a tab (default)'never': Never saves hash variantssaveHashVariantToLocalStorage: 'never' to prevent hash variants from being saveduseUIState is receiving the mainSlug parameter for auto-expansion