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:
The hook is built using a modular architecture with six specialized sub-hooks:
import { useCode } from '@mui/internal-docs-infra';
import type { ContentProps } from '@mui/internal-docs-infra/CodeHighlighter/types';
function CodeContent(props: ContentProps<{}>) {
const code = useCode(props, {
defaultOpen: true,
initialVariant: 'TypeScript',
});
return (
<div>
<h3>{code.userProps.name}</h3>
<div>
Current: {code.selectedVariant}
<select value={code.selectedVariant} onChange={(e) => code.selectVariant(e.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>
);
}
contentProps: ContentProps<T>The content properties from your CodeHighlighter component - typically passed directly from props.
opts?: UseCodeOptsOptional configuration object for customizing hook behavior.
interface UseCodeOpts {
defaultOpen?: boolean; // Whether to start expanded
copy?: any; // Copy functionality options
initialVariant?: string; // Initially selected variant
initialTransform?: string; // Initially selected transform
fileHashMode?: 'remove-hash' | 'remove-filename'; // Controls hash removal on user interaction
saveHashVariantToLocalStorage?: 'on-load' | 'on-interaction' | 'never'; // When to persist hash variant
sourceEnhancers?: SourceEnhancers; // Functions to enhance parsed HAST sources
}
The hook returns a code object with the following properties:
variants: string[] - Array of available variant keysselectedVariant: string - Currently selected variant keyselectVariant: React.Dispatch<React.SetStateAction<string>> - Function to change variantfiles: Array<{ name: string; slug?: string; component: React.ReactNode }> - Available files in current variant with optional URL slugsselectedFile: React.ReactNode - Currently selected file componentselectedFileName: string | undefined - Name of currently selected fileselectFileName: (fileName: string) => void - Function to select a file (automatically updates URL hash)
<a> tags with href={"#" + file.slug} for no-JS supporte.preventDefault() after hydration:target and :has() selectors to show/hide tabs before JS loadsexpanded: boolean - Whether the code view is expandedexpand: () => void - Function to expand the code viewsetExpanded: React.Dispatch<React.SetStateAction<boolean>> - Function to set expansion statecopy: (event: React.MouseEvent<HTMLButtonElement>) => Promise<void> - Function to copy code to clipboardavailableTransforms: string[] - Array of available transform keysselectedTransform: string | null | undefined - Currently selected transformselectTransform: (transformName: string | null) => void - Function to select a transformsetSource?: (source: string) => void - Function to update source code (when available)userProps: UserProps<T> - Generated user properties including name, slug, and custom propsThe 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={(e) => code.selectTransform(e.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={(e) => {
e.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:
fileHashModeThe 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 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, {
defaultOpen: true, // Start expanded
initialVariant: 'TypeScript', // Pre-select variant
initialTransform: 'js', // Pre-apply transform
});
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