UNPKG

react-shiki

Version:

Syntax highlighter component for react using shiki

471 lines (375 loc) 14.5 kB
# 🎨 [react-shiki](https://npmjs.com/react-shiki) > [!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](https://shiki.matsu.io/). [See the demo page with highlighted code blocks showcasing several Shiki themes!](https://react-shiki.vercel.app/) <!--toc:start--> - 🎨 [react-shiki](https://npmjs.com/react-shiki) - [Features](#features) - [Installation](#installation) - [Usage](#usage) - [Common Configuration Options](#common-configuration-options) - [Component-specific Props](#component-specific-props) - [Integration with react-markdown](#integration-with-react-markdown) - [Handling Inline Code](#handling-inline-code) - [Multi-theme Support](#multi-theme-support) - [Custom Themes](#custom-themes) - [Custom Languages](#custom-languages) - [Preloading Custom Languages](#preloading-custom-languages) - [Custom Transformers](#custom-transformers) - [Performance](#performance) - [Throttling Real-time Highlighting](#throttling-real-time-highlighting) - [Streaming and LLM Chat UI](#streaming-and-llm-chat-ui) <!--toc:end--> ## 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 ```bash npm i react-shiki ``` ## Usage 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 ( <ShikiHighlighter language="jsx" theme="ayu-dark"> {code.trim()} </ShikiHighlighter> ); } ``` **Using the Hook:** ```tsx import { useShikiHighlighter } from "react-shiki"; function CodeBlock({ code, language }) { const highlightedCode = useShikiHighlighter(code, language, "github-dark"); return <div className="code-block">{highlightedCode}</div>; } ``` ### Common Configuration Options | 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) | | `transformers` | `array` | `[]` | Custom Shiki transformers for modifying the highlighting output | | `customLanguages` | `array` | `[]` | Array of custom languages to preload | | `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 | ### Component-specific Props 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 | ### Integration with react-markdown 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 ? ( <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: ```tsx <ReactMarkdown components={{ code: CodeHighlight, }} > {markdown} </ReactMarkdown> ``` ### Handling Inline Code 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 { 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 `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 `<code>` is nested within a `<pre>` 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"; <ReactMarkdown rehypePlugins={[rehypeInlineCodeProperty]} components={{ code: CodeHighlight, }} > {markdown} </ReactMarkdown>; ``` 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 ? ( <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: ```tsx <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: ```tsx const highlightedCode = useShikiHighlighter( code, "tsx", { light: "github-light", dark: "github-dark", dim: "github-dark-dimmed", }, { defaultColor: "dark", } ); ``` See [shiki's documentation](https://shiki.matsu.io/docs/themes) 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](https://github.com/antfu/textmate-grammars-themes/blob/main/packages/tm-themes/themes/dark-plus.json). ```tsx import tokyoNight from "../styles/tokyo-night.json"; // Using the component <ShikiHighlighter language="tsx" theme={tokyoNight}> {code.trim()} </ShikiHighlighter> // Using the hook 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](https://github.com/shikijs/textmate-grammars-themes/blob/main/packages/tm-grammars/grammars/typescript.json) ```tsx import mcfunction from "../langs/mcfunction.tmLanguage.json"; // Using the component <ShikiHighlighter language={mcfunction} theme="github-dark"> {code.trim()} </ShikiHighlighter> // Using the 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"; // With the component <ShikiHighlighter language="typescript" theme="github-dark" customLanguages={[mcfunction, bosque]} > {code.trim()} </ShikiHighlighter> // With the hook const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", { customLanguages: [mcfunction, bosque], }); ``` ### Custom Transformers ```tsx import { customTransformer } from "../utils/shikiTransformers"; // Using the component <ShikiHighlighter language="tsx" transformers={[customTransformer]}> {code.trim()} </ShikiHighlighter> // Using the hook const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", { transformers: [customTransformer], }); ``` ## Performance ### Throttling Real-time Highlighting For improved performance when highlighting frequently changing code: ```tsx // With the component <ShikiHighlighter language="tsx" theme="github-dark" delay={150}> {code.trim()} </ShikiHighlighter> // With the hook 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: ```tsx 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: ```tsx 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: ```tsx 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> ); }; ```