Automatic embedded language highlighting
mainguessEmbeddedLanguages. No extra configuration is required for standard fenced code blocks.repository·main·Indexed 19 days ago
https://github.com/avgvstvs96/react-shikiA performant client-side syntax highlighting library for React powered by Shiki. It provides the ShikiHighlighter component and useShikiHighlighter hook to render highlighted code with support for custom themes, languages, and multiple bundle options (Full, Web, and Core) to optimize bundle size. Features include multi-theme support with light-dark() reactivity, configurable RegExp engines (Oniguruma, JavaScript RegExp, and JavaScript Raw), and support for custom TextMate grammars and transformers.
guessEmbeddedLanguages. No extra configuration is required for standard fenced code blocks.Depending on your bundle size requirements and language support needs, you can choose from three different entry points:
react-shiki (Full Bundle): Best for unknown language requirements. Includes all Shiki languages and themes. (~1.2MB gzipped).react-shiki/web (Web Bundle): Best for web applications. Includes web-focused languages like HTML, CSS, JS, TS, JSON, Markdown, Vue, JSX, and Svelte. (~707KB gzipped).react-shiki/core (Minimal Bundle): Best for production apps requiring maximum control. You must manually import and configure themes, languages, and an engine. (~12KB + imports).// Full Bundle
import ShikiHighlighter from 'react-shiki';
// Web Bundle
import ShikiHighlighter from 'react-shiki/web';
// Core Bundle
import ShikiHighlighter, {
createHighlighterCore,
createOnigurumaEngine,
createJavaScriptRegexEngine,
} from 'react-shiki/core';To use react-shiki in your React project, install the package using npm:
npm i react-shikiTo use react-shiki for syntax highlighting within react-markdown, create a custom code component. This component should extract the language from the className (e.g., language-tsx) and use ShikiHighlighter for code blocks, while falling back to a standard <code> tag for inline code.
Note: Since react-markdown v9.0.0, the inline prop was removed. You must use either the isInlineCode helper or the rehypeInlineCodeProperty plugin to distinguish between inline and block code.
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 ? (
<ShikiHighlighter language={language} theme="github-dark" {...props}>
{code}
</ShikiHighlighter>
) : (
<code className={className} {...props}>
{code}
</code>
);
};
<ReactMarkdown
components={{
code: CodeHighlight,
}}
>
{markdown}
</ReactMarkdown>The recommended way to make themes reactive to the user's system preference is to set defaultColor="light-dark()". This utilizes the CSS light-dark() function.
Note: This requires the site to set the color-scheme CSS property.
/* Required CSS */
:root {
color-scheme: light dark;
}
/* For class-based dark mode */
:root.dark {
color-scheme: dark;
}// Component
<ShikiHighlighter
language="tsx"
theme={{
light: "github-light",
dark: "github-dark",
}}
defaultColor="light-dark()"
>
{code.trim()}
</ShikiHighlighter>
// Hook
const highlightedCode = useShikiHighlighter(code, "tsx", {
light: "github-light",
dark: "github-dark",
}, {
defaultColor: "light-dark()",
});<ShikiHighlighter
language="tsx"
theme={{
light: "github-light",
dark: "github-dark",
}}
defaultColor="light-dark()"
>
{code.trim()}
</ShikiHighlighter>If you are highlighting languages dynamically at runtime, you should preload your custom language grammars to ensure they are available.
import mcfunction from "../langs/mcfunction.tmLanguage.json";
import bosque from "../langs/bosque.tmLanguage.json";
// Component
<ShikiHighlighter
language="typescript"
theme="github-dark"
preloadLanguages={[mcfunction, bosque]}
>
{code.trim()}
</ShikiHighlighter>
// Hook
const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", {
preloadLanguages: [mcfunction, bosque],
});NOTE Bundled languages are loaded on demand and do not need to be preloaded.
Line numbers are CSS-based and can be enabled via showLineNumbers. You can also specify which lines to highlight using highlightLineNumbers.
// Component
<ShikiHighlighter
language="javascript"
theme="github-dark"
showLineNumbers
startingLineNumber={0} // default is 1
highlightLineNumbers={[2, 4]}
>
{code}
</ShikiHighlighter>
// Hook
const highlightedCode = useShikiHighlighter(code, "javascript", "github-dark", {
showLineNumbers: true,
startingLineNumber: 0,
highlightLineNumbers: [2, 4],
});NOTE No CSS import is needed. The hook injects line-number and highlight styles automatically when enabled. To customize them, override the following CSS variables or classes:
.rs-line-number(linespan).rs-highlighted-line(linespan).rs-has-line-numbers/.rs-has-highlighted-lines(containercodeelement)
highlightLineNumbersuses displayed line numbers, so it works in conjunction withstartingLineNumber.
For production applications, it is recommended to enable type-aware lint rules in your ESLint configuration. This requires configuring parserOptions to point to your tsconfig files and switching to type-checked plugin recommendations.
parserOptions in your ESLint config file:ecmaVersion to 'latest'.sourceType to 'module'.tsconfig files in project.tsconfigRootDir to __dirname.extends list:plugin:@typescript-eslint/recommended with plugin:@typescript-eslint/recommended-type-checked or plugin:@typescript-eslint/strict-type-checked.plugin:@typescript-eslint/stylistic-type-checked.eslint-plugin-react and add plugin:react/recommended and plugin:react/jsx-runtime to the extends list.export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: __dirname,
},
}When using the react-shiki/core bundle, you must create a custom highlighter instance using createHighlighterCore. This allows you to dynamically import only the specific themes, languages, and engines you need, significantly reducing the client-side bundle size.
import ShikiHighlighter, {
createHighlighterCore,
createOnigurumaEngine,
createJavaScriptRegexEngine,
} from 'react-shiki/core';
// Create custom highlighter with dynamic imports
const highlighter = await createHighlighterCore({
themes: [import('@shikijs/themes/nord')],
langs: [import('@shikijs/langs/typescript')],
engine: createOnigurumaEngine(import('shiki/wasm'))
// or createJavaScriptRegexEngine()
});
// Pass the custom highlighter to the component
<ShikiHighlighter highlighter={highlighter} language="typescript" theme="nord">
{code}
</ShikiHighlighter>To use react-shiki within react-markdown, create a custom component that checks if the node is inline code or a code block. Use isInlineCode from react-shiki to distinguish between them.
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 ? (
<ShikiHighlighter language={language} theme="catppuccin-mocha" {...props}>
{code}
</ShikiHighlighter>
) : (
<code className={className} {...props}>
{code}
</code>
);
};Because react-markdown v9.0.0 removed the inline prop, react-shiki provides two methods to identify inline code:
isInlineCode helperPass the node object from react-markdown to the isInlineCode function. It identifies inline code by checking for the absence of newline characters.
rehypeInlineCodeProperty pluginAdd rehypeInlineCodeProperty to your rehypePlugins array in ReactMarkdown. This plugin adds an inline prop to your code components by checking if the <code> tag is nested within a <pre> tag.
Example using the plugin:
import ReactMarkdown from "react-markdown";
import { rehypeInlineCodeProperty } from "react-shiki";
// In your component:
const CodeHighlight = ({ inline, className, children, ...props }) => {
const code = String(children).trim();
const match = className?.match(/language-(\w+)/);
const language = match ? match[1] : undefined;
return !inline ? (
<ShikiHighlighter language={language} theme="github-dark" {...props}>
{code}
</ShikiHighlighter>
) : (
<code className={className} {...props}>
{code}
</code>
);
};
// In your render:
<ReactMarkdown
rehypePlugins={[rehypeInlineCodeProperty]}
components={{
code: CodeHighlight,
}}
>
{markdown}
</ReactMarkdown>To optimize client-side bundle size, use react-shiki/core to create a custom highlighter instance. This allows you to dynamically import only the specific themes, languages, and engines you need.
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/ayu-dark')],
langs: [import('@shikijs/langs/typescript')],
engine: createOnigurumaEngine(import('shiki/wasm'))
// or createJavaScriptRegexEngine()
});
<ShikiHighlighter highlighter={highlighter} language="typescript" theme="ayu-dark">
{code.trim()}
</ShikiHighlighter>