# 🎨 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](#🎨-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)
      - [Using Engines with Full and Web Bundles](#using-engines-with-full-and-web-bundles)
      - [Using Engines with Core Bundle](#using-engines-with-core-bundle)
      - [Engine Options](#engine-options)
  - [Configuration](#configuration)
    - [Common Configuration Options](#common-configuration-options)
    - [Component-specific Props](#component-specific-props)
    - [Embedded Language Highlighting](#embedded-language-highlighting)
    - [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 Languages](#preloading-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)
<!--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
- 🧬 Automatic highlighting of embedded languages (e.g. TypeScript fenced inside Markdown) via Shiki's `guessEmbeddedLanguages`
- 🔧 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`            | `[]`            | **Deprecated**: use `preloadLanguages` instead. |
| `preloadLanguages`  | `array`            | `[]`            | Preload bundled language IDs and custom language grammars |
| `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`  | `'div'` | 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                 |

### Embedded Language Highlighting

react-shiki uses Shiki's [`guessEmbeddedLanguages`](https://shiki.style) to automatically detect, load, and highlight languages nested inside other languages — for example, a TypeScript fenced code block inside a Markdown document will be highlighted as TypeScript without any extra configuration. Embedded languages are dynamically loaded on demand, as long as they exist in your bundle.

### 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 Languages

If highlighting is dynamic at runtime, preload custom languages. 

```tsx
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 language are dynamically loaded as needed, and do not need to be preloaded as long as they exist in your bundle.

### Language Aliases

You can define custom aliases for languages using the `langAlias` option. This is useful when you need to support 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 or provide your own CSS 
> for `.rs-line-number` (line `span`) and `.rs-has-line-numbers` (container `code` element). <!-- TODO: The CSS vars/classes/selectors are poorly documented -->
> ```tsx
> import 'react-shiki/css';
> ```

> [!WARNING]
> Legacy `.line-numbers` / `.has-line-numbers` selectors and unprefixed line-number CSS variables are removed as of 0.10.1.

Component-internal default classes are namespaced under `rs-*` and shipped inside `@layer base` so app-level utilities can override them more predictably.

Available CSS variables for customization:
```css
--rs-line-numbers-foreground: rgba(107, 114, 128, 0.5);
--rs-line-numbers-width: 2ch;
--rs-line-numbers-padding-left: 0ch;
--rs-line-numbers-padding-right: 2ch;
--rs-line-numbers-font-size: inherit;
--rs-line-numbers-font-weight: inherit;
--rs-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={{
    '--rs-line-numbers-foreground': '#60a5fa',
    '--rs-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)
