UNPKG

react-shiki

Version:

Syntax highlighter component for react using shiki

657 lines (524 loc) 22.1 kB
# 🎨 [react-shiki](https://npmjs.com/react-shiki) 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) - [Bundle Options](#bundle-options) - [`react-shiki` (Full Bundle)](#react-shiki-full-bundle) - [`react-shiki/web` (Web Bundle)](#react-shikiweb-web-bundle) - [`react-shiki/core` (Minimal Bundle)](#react-shikicore-minimal-bundle) - [RegExp Engines](#regexp-engines) - [Configuration](#configuration) - [Common Configuration Options](#common-configuration-options) - [Component-specific Props](#component-specific-props) - [Multi-theme Support](#multi-theme-support) - [Making Themes Reactive](#making-themes-reactive) - [Option 1: Using `light-dark()` Function (Recommended)](#option-1-using-light-dark-function-recommended) - [Option 2: CSS Theme Switching](#option-2-css-theme-switching) - [Custom Themes](#custom-themes) - [Custom Languages](#custom-languages) - [Preloading Custom Languages](#preloading-custom-languages) - [Language Aliases](#language-aliases) - [Custom Transformers](#custom-transformers) - [Line Numbers](#line-numbers) - [Integration](#integration) - [Integration with react-markdown](#integration-with-react-markdown) - [Handling Inline Code](#handling-inline-code) - [Performance](#performance) - [Throttling Real-time Highlighting](#throttling-real-time-highlighting) - [Output Format Optimization](#output-format-optimization) - [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 - 🔐 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 ## 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>; } ``` ## Bundle Options `react-shiki`, like `shiki`, offers three entry points to balance convenience and bundle optimization: ### `react-shiki` (Full Bundle) ```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 ### `react-shiki/web` (Web Bundle) ```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 ### `react-shiki/core` (Minimal Bundle) ```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() }); <ShikiHighlighter highlighter={highlighter} language="typescript" theme="nord"> {code} </ShikiHighlighter> ``` - **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) ### RegExp Engines 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](https://shiki.style/guide/regex-engines#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 <ShikiHighlighter language="typescript" theme="github-dark" engine={createJavaScriptRawEngine()} > {code} </ShikiHighlighter> ``` #### 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](https://shiki.style/guide/regex-engines#use-with-unsupported-languages). For best-effort results with unsupported grammars, enable the `forgiving` option: ```tsx createJavaScriptRegexEngine({ forgiving: true }); ``` See [Shiki - RegExp Engines](https://shiki.style/guide/regex-engines) for more info. ## Configuration ### 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) | | `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`](https://github.com/shikijs/shiki/blob/main/packages/types/src/options.ts#L121) | - | - | All other options supported by Shiki's `codeToHast` | ### 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 | ### 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", } ); ``` #### 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 <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()" }); ``` 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](https://shiki.matsu.io/guide/dual-themes) for the required CSS snippets. > **Note**: The `light-dark()` function requires modern browser support. For older browsers, use the manual CSS variables approach. ### 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"; // Component <ShikiHighlighter language="tsx" theme={tokyoNight}> {code.trim()} </ShikiHighlighter> // 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"; // Component <ShikiHighlighter language={mcfunction} theme="github-dark"> {code.trim()} </ShikiHighlighter> // 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 <ShikiHighlighter language="typescript" theme="github-dark" customLanguages={[mcfunction, bosque]} > {code.trim()} </ShikiHighlighter> // Hook const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", { customLanguages: [mcfunction, bosque], }); ``` ### Language Aliases 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 <ShikiHighlighter language="indents" theme="github-dark" langAlias={{ indents: "python" }} > {code.trim()} </ShikiHighlighter> // Hook const highlightedCode = useShikiHighlighter(code, "indents", "github-dark", { langAlias: { indents: "python" }, }); ``` ### Custom Transformers ```tsx import { customTransformer } from "../utils/shikiTransformers"; // Component <ShikiHighlighter language="tsx" transformers={[customTransformer]}> {code.trim()} </ShikiHighlighter> // Hook const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", { transformers: [customTransformer], }); ``` ### Line Numbers Display line numbers alongside your code, these are CSS-based and can be customized with CSS variables: ```tsx // Component <ShikiHighlighter language="javascript" theme="github-dark" showLineNumbers, startingLineNumber={0} // default is 1 > {code} </ShikiHighlighter> <ShikiHighlighter language="python" theme="github-dark" showLineNumbers startingLineNumber={0} > {code} </ShikiHighlighter> // 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 <ShikiHighlighter language="javascript" theme="github-dark" showLineNumbers style={{ '--line-numbers-foreground': '#60a5fa', '--line-numbers-width': '3ch' }} > {code} </ShikiHighlighter> ``` ## Integration ### 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 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 ? ( <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> ); }; ``` ## 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, }); ``` ### Output Format Optimization `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 <ShikiHighlighter language="tsx" theme="github-dark"> {code} </ShikiHighlighter> ``` **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') <ShikiHighlighter language="tsx" theme="github-dark" outputFormat="html"> {code} </ShikiHighlighter> ``` 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)](https://github.com/AVGVSTVS96)