Syntax highlighter component for react using shiki
npm install react-shiki
A performant client-side syntax highlighting component and hook for React, built with Shiki.
See the demo page with highlighted code blocks showcasing several Shiki themes!
- 🎨 react-shiki
- Features
- Installation
- Usage
- Bundle Options
- react-shiki (Full Bundle)
- react-shiki/web (Web Bundle)
- react-shiki/core (Minimal Bundle)
- RegExp Engines
- Configuration
- Common Configuration Options
- Component-specific Props
- Multi-theme Support
- Making Themes Reactive
- Option 1: Using light-dark() Function (Recommended)
- Option 2: CSS Theme Switching
- Custom Themes
- Custom Languages
- Preloading Custom Languages
- Language Aliases
- Custom Transformers
- Line Numbers
- Integration
- Integration with react-markdown
- Handling Inline Code
- Performance
- Throttling Real-time Highlighting
- Output Format Optimization
- Streaming and LLM Chat UI
- 🖼️ Provides both a ShikiHighlighter component and a useShikiHighlighter hook for more flexibility
- 🔐 Flexible output: Choose between React elements (no dangerouslySetInnerHTML) or HTML strings for better performance
- 📦 Multiple bundle options: Full bundle (~1.2MB gz), web bundle (~695KB gz), or minimal core bundle for fine-grained bundle control
- 🖌️ Full support for custom TextMate themes and languages
- 🔧 Supports passing custom Shiki transformers to the highlighter, in addition to all other options supported by codeToHast
- 🚰 Performant highlighting of streamed code, with optional throttling
- 📚 Includes minimal default styles for code blocks
- 🚀 Shiki dynamically imports only the languages and themes used on a page for optimal performance
- 🖥️ ShikiHighlighter component displays a language label for each code block
when showLanguage is set to true (default)
- 🎨 Customizable styling of generated code blocks and language labels
- 📏 Optional line numbers with customizable starting number and styling
``bash`
npm i react-shiki
You can use either the ShikiHighlighter component or the useShikiHighlighter hook to highlight code.
Using the Component:
`tsx
import ShikiHighlighter from "react-shiki";
function CodeBlock() {
return (
{code.trim()}
);
}
`
Using the Hook:
`tsx
import { useShikiHighlighter } from "react-shiki";
function CodeBlock({ code, language }) {
const highlightedCode = useShikiHighlighter(code, language, "github-dark");
return
Bundle Options
react-shiki, like shiki, offers three entry points to balance convenience and bundle optimization:$3
`tsx
import ShikiHighlighter from 'react-shiki';
`
- Size: ~6.4MB minified, ~1.2MB gzipped (includes ~12KB react-shiki)
- Languages: All Shiki languages and themes
- Exported engines: createJavaScriptRegexEngine, createJavaScriptRawEngine
- Use case: Unknown language requirements, maximum language support
- Setup: Zero configuration required$3
`tsx
import ShikiHighlighter from 'react-shiki/web';
`
- Size: ~3.8MB minified, ~707KB gzipped (includes ~12KB react-shiki)
- Languages: Web-focused languages (HTML, CSS, JS, TS, JSON, Markdown, Vue, JSX, Svelte)
- Exported engines: createJavaScriptRegexEngine, createJavaScriptRawEngine
- Use case: Web applications with balanced size/functionality
- Setup: Drop-in replacement for main entry point$3
`tsx
import ShikiHighlighter, {
createHighlighterCore, // re-exported from shiki/core
createOnigurumaEngine, // re-exported from shiki/engine/oniguruma
createJavaScriptRegexEngine, // re-exported from shiki/engine/javascript
} from 'react-shiki/core';// Create custom highlighter with dynamic imports to optimize client-side bundle size
const highlighter = await createHighlighterCore({
themes: [import('@shikijs/themes/nord')],
langs: [import('@shikijs/langs/typescript')],
engine: createOnigurumaEngine(import('shiki/wasm'))
// or createJavaScriptRegexEngine()
});
{code}
`
- Size: ~12KB + your imported themes/languages
- Languages: User-defined via custom highlighter
- Use case: Production apps requiring maximum bundle control
- Setup: Requires custom highlighter configuration
- Engine options: Choose JavaScript engine (smaller bundle, faster startup) or Oniguruma (WASM, maximum language support)$3
Shiki offers three built-in engines for syntax highlighting:
- Oniguruma - Default engine using compiled WebAssembly, offers maximum language support
- JavaScript RegExp - Smaller bundle, faster startup, compiles patterns on-the-fly, recommended for client-side highlighting
- JavaScript Raw - For pre-compiled languages, skips transpilation step for best performance
#### Using Engines with Full and Web Bundles
The full and web bundles use Oniguruma by default, but you can override this with the
engine option:`tsx
import {
useShikiHighlighter,
createJavaScriptRegexEngine,
createJavaScriptRawEngine
} from 'react-shiki';// Hook with JavaScript RegExp engine
const highlightedCode = useShikiHighlighter(code, 'typescript', 'github-dark', {
engine: createJavaScriptRegexEngine()
});
// Component with JavaScript Raw engine (for pre-compiled languages)
// See https://shiki.style/guide/regex-engines#pre-compiled-languages
language="typescript"
theme="github-dark"
engine={createJavaScriptRawEngine()}
>
{code}
`#### Using Engines with Core Bundle
When using the core bundle, you must specify an engine:
`tsx
import {
createHighlighterCore,
createOnigurumaEngine,
createJavaScriptRegexEngine
} from 'react-shiki/core';const highlighter = await createHighlighterCore({
themes: [import('@shikijs/themes/nord')],
langs: [import('@shikijs/langs/typescript')],
engine: createJavaScriptRegexEngine() // or createOnigurumaEngine(import('shiki/wasm'))
});
`#### Engine Options
The JavaScript RegExp engine is strict by default. For best-effort results with unsupported grammars, enable the
forgiving option:`tsx
createJavaScriptRegexEngine({ forgiving: true });
`See Shiki - RegExp Engines for more info.
Configuration
$3
| Option | Type | Default | Description |
| ------------------- | ------------------ | --------------- | ----------------------------------------------------------------------------- |
|
code | string | - | Code to highlight |
| language | string \| object | - | Language to highlight, built-in or custom textmate grammer object |
| theme | string \| object | 'github-dark' | Single or multi-theme configuration, built-in or custom textmate theme object |
| delay | number | 0 | Delay between highlights (in milliseconds) |
| customLanguages | array | [] | Array of custom languages to preload |
| langAlias | object | {} | Map of language aliases |
| engine | RegexEngine | Oniguruma | RegExp engine for syntax highlighting (Oniguruma, JavaScript RegExp, or JavaScript Raw) |
| showLineNumbers | boolean | false | Display line numbers alongside code |
| startingLineNumber | number | 1 | Starting line number when line numbers are enabled |
| transformers | array | [] | Custom Shiki transformers for modifying the highlighting output |
| cssVariablePrefix | string | '--shiki' | Prefix for CSS variables storing theme colors |
| defaultColor | string \| false | 'light' | Default theme mode when using multiple themes, can also disable default theme |
| outputFormat | string | 'react' | Output format: 'react' for React nodes, 'html' for HTML string |
| tabindex | number | 0 | Tab index for the code block |
| decorations | array | [] | Custom decorations to wrap the highlighted tokens with |
| structure | string | classic | The structure of the generated HAST and HTML - classic or inline |
| codeToHastOptions | - | - | All other options supported by Shiki's codeToHast |$3
The
ShikiHighlighter component offers minimal built-in styling and customization options out-of-the-box:| Prop | Type | Default | Description |
| ------------------ | --------- | ------- | ---------------------------------------------------------- |
|
showLanguage | boolean | true | Displays language label in top-right corner |
| addDefaultStyles | boolean | true | Adds minimal default styling to the highlighted code block |
| as | string | 'pre' | Component's Root HTML element |
| className | string | - | Custom class name for the code block |
| langClassName | string | - | Class name for styling the language label |
| style | object | - | Inline style object for the code block |
| langStyle | object | - | Inline style object for the language label |$3
To use multiple theme modes, pass an object with your multi-theme configuration to the
theme prop in the ShikiHighlighter component:`tsx
language="tsx"
theme={{
light: "github-light",
dark: "github-dark",
dim: "github-dark-dimmed",
}}
defaultColor="dark"
>
{code.trim()}
`Or, when using the hook, pass it to the
theme parameter:`tsx
const highlightedCode = useShikiHighlighter(
code,
"tsx",
{
light: "github-light",
dark: "github-dark",
dim: "github-dark-dimmed",
},
{
defaultColor: "dark",
}
);
`#### Making Themes Reactive
There are two approaches to make multi-themes reactive to your site's theme:
##### Option 1: Using
light-dark() Function (Recommended)Set
defaultColor="light-dark()" to use CSS's built-in light-dark() function. This automatically switches themes based on the user's color-scheme preference:`tsx
// Component
language="tsx"
theme={{
light: "github-light",
dark: "github-dark",
}}
defaultColor="light-dark()"
>
{code.trim()}
// Hook
const highlightedCode = useShikiHighlighter(code, "tsx", {
light: "github-light",
dark: "github-dark",
}, {
defaultColor: "light-dark()"
});
`Ensure your site sets the
color-scheme CSS property:
`css
:root {
color-scheme: light dark;
}/ Or dynamically for class based dark mode /
:root {
color-scheme: light;
}
:root.dark {
color-scheme: dark;
}
`##### Option 2: CSS Theme Switching
For broader browser support or more control, add CSS snippets to your site to enable theme switching with media queries or class-based switching. See Shiki's documentation for the required CSS snippets.
> Note: The
light-dark() function requires modern browser support. For older browsers, use the manual CSS variables approach.$3
Custom themes can be passed as a TextMate theme in JavaScript object. For example, it should look like this.
`tsx
import tokyoNight from "../styles/tokyo-night.json";// Component
{code.trim()}
// Hook
const highlightedCode = useShikiHighlighter(code, "tsx", tokyoNight);
`$3
Custom languages should be passed as a TextMate grammar in JavaScript object. For example, it should look like this
`tsx
import mcfunction from "../langs/mcfunction.tmLanguage.json";// Component
{code.trim()}
// Hook
const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark");
`#### Preloading Custom Languages
For dynamic highlighting scenarios where language selection happens at runtime:
`tsx
import mcfunction from "../langs/mcfunction.tmLanguage.json";
import bosque from "../langs/bosque.tmLanguage.json";// Component
language="typescript"
theme="github-dark"
customLanguages={[mcfunction, bosque]}
>
{code.trim()}
// Hook
const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", {
customLanguages: [mcfunction, bosque],
});
`$3
You can define custom aliases for languages using the
langAlias option. This is useful when you want to use alternative names for languages:`tsx
// Component
language="indents"
theme="github-dark"
langAlias={{ indents: "python" }}
>
{code.trim()}
// Hook
const highlightedCode = useShikiHighlighter(code, "indents", "github-dark", {
langAlias: { indents: "python" },
});
`$3
`tsx
import { customTransformer } from "../utils/shikiTransformers";// Component
{code.trim()}
// Hook
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
transformers: [customTransformer],
});
`$3
Display line numbers alongside your code, these are CSS-based
and can be customized with CSS variables:
`tsx
// Component
language="javascript"
theme="github-dark"
showLineNumbers,
startingLineNumber={0} // default is 1
>
{code}
language="python"
theme="github-dark"
showLineNumbers
startingLineNumber={0}
>
{code}
// Hook (import 'react-shiki/css' for line numbers to work)
const highlightedCode = useShikiHighlighter(code, "javascript", "github-dark", {
showLineNumbers: true,
startingLineNumber: 0,
});
`> [!NOTE]
> When using the hook with line numbers, import the CSS file for the line numbers to work:
>
`tsx
> import 'react-shiki/css';
> `
> Or provide your own CSS counter implementation and styles for .line-numbers (line span) and .has-line-numbers (container code element)Available CSS variables for customization:
`css
--line-numbers-foreground: rgba(107, 114, 128, 0.5);
--line-numbers-width: 2ch;
--line-numbers-padding-left: 0ch;
--line-numbers-padding-right: 2ch;
--line-numbers-font-size: inherit;
--line-numbers-font-weight: inherit;
--line-numbers-opacity: 1;
`You can customize them in your own CSS or by using the style prop on the component:
`tsx
language="javascript"
theme="github-dark"
showLineNumbers
style={{
'--line-numbers-foreground': '#60a5fa',
'--line-numbers-width': '3ch'
}}
>
{code}
`Integration
$3
Create a component to handle syntax highlighting:
`tsx
import ReactMarkdown from "react-markdown";
import ShikiHighlighter, { isInlineCode } from "react-shiki";const CodeHighlight = ({ className, children, node, ...props }) => {
const code = String(children).trim();
const match = className?.match(/language-(\w+)/);
const language = match ? match[1] : undefined;
const isInline = node ? isInlineCode(node) : undefined;
return !isInline ? (
{code}
) : (
{code}
);
};
`Pass the component to react-markdown as a code component:
`tsx
components={{
code: CodeHighlight,
}}
>
{markdown}
`$3
Prior to
9.0.0, react-markdown exposed the inline prop to code
components which helped to determine if code is inline. This functionality was
removed in 9.0.0. For your convenience, react-shiki provides two
ways to replicate this functionality and API.Method 1: Using the
isInlineCode helper:react-shiki exports isInlineCode which parses the node prop from react-markdown and identifies inline code by checking for the absence of newline characters:`tsx
import ShikiHighlighter, { isInlineCode } from "react-shiki";const CodeHighlight = ({ className, children, node, ...props }) => {
const match = className?.match(/language-(\w+)/);
const language = match ? match[1] : undefined;
const isInline = node ? isInlineCode(node) : undefined;
return !isInline ? (
{String(children).trim()}
) : (
{children}
);
};
`Method 2: Using the
rehypeInlineCodeProperty plugin:react-shiki also exports rehypeInlineCodeProperty, a rehype plugin that
provides the same API as react-markdown prior to 9.0.0. It reintroduces the
inline prop which works by checking if is nested within a tag,
if not, it's considered inline code and the inline prop is set to true.It's passed as a
rehypePlugin to react-markdown:`tsx
import ReactMarkdown from "react-markdown";
import { rehypeInlineCodeProperty } from "react-shiki"; rehypePlugins={[rehypeInlineCodeProperty]}
components={{
code: CodeHighlight,
}}
>
{markdown}
;
`Now
inline can be accessed as a prop in the code component:`tsx
const CodeHighlight = ({
inline,
className,
children,
node,
...props
}: CodeHighlightProps): JSX.Element => {
const match = className?.match(/language-(\w+)/);
const language = match ? match[1] : undefined;
const code = String(children).trim(); return !inline ? (
{code}
) : (
{code}
);
};
`Performance
$3
For improved performance when highlighting frequently changing code:
`tsx
// With the component
{code.trim()}
// With the hook
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
delay: 150,
});
`$3
react-shiki provides two output formats to balance safety and performance:React Nodes (Default) - Safer, no
dangerouslySetInnerHTML required
`tsx
// Hook
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark");// Component
{code}
`HTML String - 15-45% faster performance
`tsx
// Hook (returns HTML string, use dangerouslySetInnerHTML to render)
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
outputFormat: 'html'
});// Component (automatically uses dangerouslySetInnerHTML when outputFormat is 'html')
{code}
``Choose HTML output when performance is critical and you trust the code source. Use the default React output when handling untrusted content or when security is the primary concern.
---
Made with ❤️ by Bassim (AVGVSTVS96)