[!NOTE]
This library is still in development. More features will be implemented, and the API may change.
Contributions are welcome!
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!
Features
- 🖼️ Provides both a
ShikiHighlighter
component and a useShikiHighlighter
hook for more flexibility
- 🔐 No
dangerouslySetInnerHTML
- output from Shiki is parsed using html-react-parser
- 📦 Supports all built-in Shiki languages and themes
- 🖌️ Full support for custom TextMate themes and languages
- 🔧 Supports passing custom Shiki transformers to the highlighter
- 🚰 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
Installation
npm install react-shiki
Usage
Basic Usage
You can use either the ShikiHighlighter
component or the useShikiHighlighter
hook to highlight code.
Using the Component:
import { ShikiHighlighter } from 'react-shiki';
function CodeBlock() {
return (
<ShikiHighlighter language="jsx" theme="ayu-dark">
{code.trim()}
</ShikiHighlighter>
);
}
The ShikiHighlighter
component accepts the following props:
language | string | - | Language of the code to highlight |
theme | string | object | 'github-dark' | Shiki theme to use |
showLanguage | boolean | true | Shows the language name in the top right corner |
addDefaultStyles | boolean | true | Adds default styling to the code block |
as | string | 'pre' | Root element to render |
delay | number | 0 | Delay between highlights in milliseconds |
customLanguages | array | - | Custom languages to preload |
transformers | array | - | Custom Shiki transformers |
className | string | - | Custom class names for the component |
langClassName | string | - | Class names for the language label |
style | object | - | Inline style object for the component |
langStyle | object | - | Inline style object for the language label |
Using the Hook:
import { useShikiHighlighter } from 'react-shiki';
function CustomCodeBlock({ code, language }) {
const highlightedCode = useShikiHighlighter(code, language, 'github-dark');
return <div className="custom-code-block">{highlightedCode}</div>;
}
The hook accepts the following parameters:
code | string | The code to be highlighted |
language | string | object | The language for highlighting |
themeInput | string | object | The theme or themes to be used for highlighting |
options | object | Optional configuration options |
options
:
delay | number | 0 (disabled) | The delay between highlights in milliseconds |
transformers | array | [] | Transformers for the Shiki pipeline |
customLanguages | array | [] | Custom languages to preload |
cssVariablePrefix | string | '--shiki' | Prefix of CSS variables used to store theme colors |
defaultColor | string | 'light' | The default theme mode when using multiple themes. Can be set to false to disable the default theme |
Integration with react-markdown
Create a component to handle syntax highlighting:
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>
);
};
Pass the component to react-markdown as a code component:
<ReactMarkdown
components={{
code: CodeHighlight,
}}
>
{markdown}
</ReactMarkdown>
Handling Inline Code
Method 1: Using the isInlineCode
helper:
There are two ways to check if a code block is inline, both provide the same result:
react-shiki
exports isInlineCode
which parses the node
prop (from react-markdown
) to determine if the code is inline:
import { isInlineCode, ShikiHighlighter } 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 ? (
<ShikiHighlighter language={language} theme="github-dark" {...props}>
{String(children).trim()}
</ShikiHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
};
Method 2: Using the rehype plugin:
react-shiki
also exports rehypeInlineCodeProperty
, a rehype plugin that adds
an inline
property to react-markdown
to determine if code is inline based on
the presence of a <pre>
tag as a parent of <code>
.
It's passed as a rehype plugin to react-markdown
:
import ReactMarkdown from "react-markdown";
import { rehypeInlineCodeProperty } from "react-shiki";
<ReactMarkdown
rehypePlugins={[rehypeInlineCodeProperty]}
components={{
code: CodeHighlight,
}}
>
{markdown}
</ReactMarkdown>;
Now inline
can be accessed as a prop in the CodeHighlight
component:
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 ? (
<ShikiHighlighter language={language} theme="github-dark" {...props}>
{code}
</ShikiHighlighter>
) : (
<code className={className} {...props}>
{code}
</code>
);
};
Multi-theme Support
To use multiple theme modes, pass an object with your multi-theme configuration to the theme
prop in the ShikiHighlighter
component:
<ShikiHighlighter
language="tsx"
theme={{
light: "github-light",
dark: "github-dark",
dim: "github-dark-dimmed",
}}
defaultColor="dark"
>
{code.trim()}
</ShikiHighlighter>
Or, when using the hook, pass it to the theme
parameter:
const highlightedCode = useShikiHighlighter(
code,
"tsx",
{
light: "github-light",
dark: "github-dark",
dim: "github-dark-dimmed"
},
{
defaultColor: "dark",
}
);
See shiki's documentation for more information on dual and multi theme support, and for the CSS needed to make the themes reactive to your site's theme.
Custom Themes
Custom themes can be passed as a TextMate theme in JavaScript object. For example, it should look like this.
import tokyoNight from "../styles/tokyo-night.json";
<ShikiHighlighter language="tsx" theme={tokyoNight}>
{code.trim()}
</ShikiHighlighter>
const highlightedCode = useShikiHighlighter(code, "tsx", tokyoNight);
Custom Languages
Custom languages should be passed as a TextMate grammar in JavaScript object. For example, it should look like this
import mcfunction from "../langs/mcfunction.tmLanguage.json";
<ShikiHighlighter language={mcfunction} theme="github-dark">
{code.trim()}
</ShikiHighlighter>
const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark");
Preloading Custom Languages
For dynamic highlighting scenarios where language selection happens at runtime:
import mcfunction from "../langs/mcfunction.tmLanguage.json";
import bosque from "../langs/bosque.tmLanguage.json";
<ShikiHighlighter
language="typescript"
theme="github-dark"
customLanguages={[mcfunction, bosque]}
>
{code.trim()}
</ShikiHighlighter>
const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", {
customLanguages: [mcfunction, bosque],
});
Custom Transformers
import { customTransformer } from "../utils/shikiTransformers";
<ShikiHighlighter language="tsx" transformers={[customTransformer]}>
{code.trim()}
</ShikiHighlighter>
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
transformers: [customTransformer],
});
Performance
Throttling Real-time Highlighting
For improved performance when highlighting frequently changing code:
<ShikiHighlighter language="tsx" theme="github-dark" delay={150}>
{code.trim()}
</ShikiHighlighter>
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
delay: 150,
});
Streaming and LLM Chat UI
react-shiki
can be used to highlight streamed code from LLM responses in real-time.
I use it for an LLM chatbot UI, it renders markdown and highlights
code in memoized chat messages.
Using useShikiHighlighter
hook:
import type { ReactNode } from "react";
import { isInlineCode, useShikiHighlighter, type Element } from "react-shiki";
import tokyoNight from "@styles/tokyo-night.mjs";
interface CodeHighlightProps {
className?: string | undefined;
children?: ReactNode | undefined;
node?: Element | undefined;
}
export const CodeHighlight = ({
className,
children,
node,
...props
}: CodeHighlightProps) => {
const code = String(children).trim();
const language = className?.match(/language-(\w+)/)?.[1];
const isInline = node ? isInlineCode(node) : false;
const highlightedCode = useShikiHighlighter(code, language, tokyoNight, {
delay: 150,
});
return !isInline ? (
<div
className="shiki not-prose relative [&_pre]:overflow-auto
[&_pre]:rounded-lg [&_pre]:px-6 [&_pre]:py-5"
>
{language ? (
<span
className="absolute right-3 top-2 text-xs tracking-tighter
text-muted-foreground/85"
>
{language}
</span>
) : null}
{highlightedCode}
</div>
) : (
<code className={className} {...props}>
{children}
</code>
);
};
Or using the ShikiHighlighter
component:
import type { ReactNode } from "react";
import ShikiHighlighter, { isInlineCode, type Element } from "react-shiki";
interface CodeHighlightProps {
className?: string | undefined;
children?: ReactNode | undefined;
node?: Element | undefined;
}
export const CodeHighlight = ({
className,
children,
node,
...props
}: CodeHighlightProps): JSX.Element => {
const match = className?.match(/language-(\w+)/);
const language = match ? match[1] : undefined;
const code = String(children).trim();
const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
return !isInline ? (
<ShikiHighlighter
language={language}
theme="github-dark"
delay={150}
{...props}
>
{code}
</ShikiHighlighter>
) : (
<code className={className}>{code}</code>
);
};
Passed to react-markdown
as a code
component in memo-ized chat messages:
const RenderedMessage = React.memo(({ message }: { message: Message }) => (
<div className={cn(messageStyles[message.role])}>
<ReactMarkdown components={{ code: CodeHighlight }}>
{message.content}
</ReactMarkdown>
</div>
));
export const ChatMessages = ({ messages }: { messages: Message[] }) => {
return (
<div className="space-y-4">
{messages.map((message) => (
<RenderedMessage key={message.id} message={message} />
))}
</div>
);
};