react-shiki 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Bassim Shahidy
3
+ Copyright (c) 2026 Bassim Shahidy
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,12 +1,9 @@
1
1
  # 🎨 react-shiki
2
2
 
3
-
4
3
  A performant client-side syntax highlighting component and hook for React, built with [Shiki](https://shiki.matsu.io/).
5
4
 
6
- [See the demo page with highlighted code blocks showcasing several Shiki themes!](https://react-shiki.vercel.app/)
7
-
8
5
  <!--toc:start-->
9
- - [🎨 react-shiki](#🎨-react-shiki)
6
+ - [🎨 react-shiki](#-react-shiki)
10
7
  - [Features](#features)
11
8
  - [Installation](#installation)
12
9
  - [Usage](#usage)
@@ -15,11 +12,14 @@ A performant client-side syntax highlighting component and hook for React, built
15
12
  - [`react-shiki/web` (Web Bundle)](#react-shikiweb-web-bundle)
16
13
  - [`react-shiki/core` (Minimal Bundle)](#react-shikicore-minimal-bundle)
17
14
  - [RegExp Engines](#regexp-engines)
18
- - [Using Engines with Full and Web Bundles](#using-engines-with-full-and-web-bundles)
19
- - [Using Engines with Core Bundle](#using-engines-with-core-bundle)
20
- - [Engine Options](#engine-options)
15
+ - [Full and Web Bundles](#full-and-web-bundles)
16
+ - [Custom Highlighters (Core Bundle)](#custom-highlighters-core-bundle)
17
+ - [JavaScript Raw Engine](#javascript-raw-engine)
18
+ - [Output Formats](#output-formats)
21
19
  - [Configuration](#configuration)
22
20
  - [Common Configuration Options](#common-configuration-options)
21
+ - [react-shiki Options](#react-shiki-options)
22
+ - [Shiki Pass-through Options](#shiki-pass-through-options)
23
23
  - [Component-specific Props](#component-specific-props)
24
24
  - [Embedded Language Highlighting](#embedded-language-highlighting)
25
25
  - [Multi-theme Support](#multi-theme-support)
@@ -37,15 +37,14 @@ A performant client-side syntax highlighting component and hook for React, built
37
37
  - [Handling Inline Code](#handling-inline-code)
38
38
  - [Performance](#performance)
39
39
  - [Throttling Real-time Highlighting](#throttling-real-time-highlighting)
40
- - [Output Format Optimization](#output-format-optimization)
41
40
  <!--toc:end-->
42
41
 
43
42
  ## Features
44
43
 
45
44
  - 🖼️ Provides both a `ShikiHighlighter` component and a `useShikiHighlighter` hook for more flexibility
46
- - 🔐 Flexible output: Choose between React elements (no `dangerouslySetInnerHTML`) or HTML strings for better performance
47
- - 📦 Multiple bundle options: Full bundle (~1.2MB gz), web bundle (~695KB gz), or minimal core bundle for fine-grained bundle control
48
- - 🖌️ Full support for custom TextMate themes and languages
45
+ - 🔐 Flexible output: Choose between React elements (no `dangerouslySetInnerHTML`) or HTML strings
46
+ - 📦 Multiple bundle options: Full bundle (~1.2MB gz), web bundle (~707KB gz), or minimal core bundle for fine-grained bundle control
47
+ - 🖌️ Full support for custom TextMate themes and languages, passed as objects or dynamic imports
49
48
  - 🧬 Automatic highlighting of embedded languages (e.g. TypeScript fenced inside Markdown) via Shiki's `guessEmbeddedLanguages`
50
49
  - 🔧 Supports passing custom Shiki transformers to the highlighter, in addition to all other options supported by `codeToHast`
51
50
  - 🚰 Performant highlighting of streamed code, with optional throttling
@@ -99,8 +98,8 @@ function CodeBlock({ code, language }) {
99
98
  ```tsx
100
99
  import ShikiHighlighter from 'react-shiki';
101
100
  ```
102
- - **Size**: ~6.4MB minified, ~1.2MB gzipped (includes ~12KB react-shiki)
103
- - **Languages**: All Shiki languages and themes
101
+ - **Size**: ~6.4MB minified, ~1.2MB gzipped (react-shiki itself: ~9KB minified, ~4KB gzipped)
102
+ - **Languages**: All Shiki [languages](https://shiki.style/languages) and [themes](https://shiki.style/themes)
104
103
  - **Exported engines**: `createJavaScriptRegexEngine`, `createJavaScriptRawEngine`
105
104
  - **Use case**: Unknown language requirements, maximum language support
106
105
  - **Setup**: Zero configuration required
@@ -109,8 +108,8 @@ import ShikiHighlighter from 'react-shiki';
109
108
  ```tsx
110
109
  import ShikiHighlighter from 'react-shiki/web';
111
110
  ```
112
- - **Size**: ~3.8MB minified, ~707KB gzipped (includes ~12KB react-shiki)
113
- - **Languages**: Web-focused languages (HTML, CSS, JS, TS, JSON, Markdown, Vue, JSX, Svelte)
111
+ - **Size**: ~3.8MB minified, ~707KB gzipped (react-shiki itself: ~9KB minified, ~4KB gzipped)
112
+ - **Languages**: [Web-focused languages](https://shiki.style/guide/bundles#shiki-bundle-web) (HTML, CSS, JS, TS, JSON, Markdown, Vue, JSX, Svelte)
114
113
  - **Exported engines**: `createJavaScriptRegexEngine`, `createJavaScriptRawEngine`
115
114
  - **Use case**: Web applications with balanced size/functionality
116
115
  - **Setup**: Drop-in replacement for main entry point
@@ -135,7 +134,7 @@ const highlighter = await createHighlighterCore({
135
134
  {code}
136
135
  </ShikiHighlighter>
137
136
  ```
138
- - **Size**: ~12KB + your imported themes/languages
137
+ - **Size**: ~9KB minified (~4KB gzipped) + your imported themes/languages
139
138
  - **Languages**: User-defined via custom highlighter
140
139
  - **Use case**: Production apps requiring maximum bundle control
141
140
  - **Setup**: Requires custom highlighter configuration
@@ -144,40 +143,57 @@ const highlighter = await createHighlighterCore({
144
143
  ### RegExp Engines
145
144
 
146
145
  Shiki offers three built-in engines for syntax highlighting:
147
- - **Oniguruma** - Default engine using compiled WebAssembly, offers maximum language support
148
- - **JavaScript RegExp** - Smaller bundle, faster startup, compiles patterns on-the-fly, recommended for client-side highlighting
149
- - **JavaScript Raw** - For [pre-compiled languages](https://shiki.style/guide/regex-engines#pre-compiled-languages), skips transpilation step for best performance
150
146
 
151
- #### Using Engines with Full and Web Bundles
147
+ | Engine | Payload (gzip) | Startup | Grammar support |
148
+ | --- | --- | --- | --- |
149
+ | **Oniguruma** (default) | ~150 KB (466 KB WASM) | fetch + compile WASM | Reference engine, all grammars |
150
+ | **JavaScript RegExp** | ~20 KB | none, plain JS | All built-in languages ([compatibility table](https://shiki.style/references/engine-js-compat)); [strict by default](#full-and-web-bundles) |
151
+ | **JavaScript Raw** | ~1 KB | none | [Pre-compiled grammars](https://shiki.style/guide/regex-engines#pre-compiled-languages) only, has a [known bug](https://github.com/shikijs/shiki/issues/918) |
152
152
 
153
- The full and web bundles use Oniguruma by default, but you can override this with the `engine` option:
153
+ Payload sizes measured against shiki 4.3.0: `onig.wasm` is 466 KB (149 KB gzipped), the JavaScript engine's pattern transpiler ([oniguruma-to-es](https://github.com/slevithan/oniguruma-to-es)) is 57 KB (20 KB gzipped), and the raw engine skips the transpiler entirely. Beyond the smaller download, the JavaScript engine also starts instantly (no WASM fetch and compile) and [runs patterns as native JavaScript RegExp](https://shiki.style/guide/regex-engines#javascript-regexp-engine), which is faster for some languages.
154
154
 
155
- ```tsx
156
- import {
157
- useShikiHighlighter,
158
- createJavaScriptRegexEngine,
159
- createJavaScriptRawEngine
160
- } from 'react-shiki';
155
+ #### Full and Web Bundles
161
156
 
162
- // Hook with JavaScript RegExp engine
157
+ The full and web bundles use Oniguruma by default. You can swap to the JavaScript engine by passing `'javascript'` to `engine`. react-shiki creates and caches the engine internally, no need to import it, no WASM fetch with `'javascript'`, and it's safe to pass inline:
158
+
159
+ ```tsx
160
+ // Hook
163
161
  const highlightedCode = useShikiHighlighter(code, 'typescript', 'github-dark', {
164
- engine: createJavaScriptRegexEngine()
162
+ engine: 'javascript'
165
163
  });
166
164
 
167
- // Component with JavaScript Raw engine (for pre-compiled languages)
168
- // See https://shiki.style/guide/regex-engines#pre-compiled-languages
169
- <ShikiHighlighter
170
- language="typescript"
171
- theme="github-dark"
172
- engine={createJavaScriptRawEngine()}
173
- >
165
+ // Component
166
+ <ShikiHighlighter language="typescript" theme="github-dark" engine="javascript">
174
167
  {code}
175
168
  </ShikiHighlighter>
176
169
  ```
177
170
 
178
- #### Using Engines with Core Bundle
171
+ > [!NOTE]
172
+ > The underlying highlighter is a singleton created on the first highlight, so pass the same engine at every call site; mixed engines resolve to whichever highlighted first.
173
+
174
+ To customize engine configuration beyond the named defaults, pass an engine instance. **Create it once at module scope**; this avoids recreating new instances on every render, which would defeat memoization:
175
+
176
+ ```tsx
177
+ import { useShikiHighlighter, createJavaScriptRegexEngine } from 'react-shiki';
178
+
179
+ // Ensure this is at the module scope: created once, stable across renders
180
+ const engine = createJavaScriptRegexEngine({ forgiving: true });
181
+
182
+ const highlightedCode = useShikiHighlighter(code, 'typescript', 'github-dark', {
183
+ engine
184
+ });
185
+ ```
186
+
187
+ The JavaScript engine is strict by default: it throws when a grammar uses patterns it can't convert. `forgiving: true` skips those patterns for best-effort output. It also accepts `target` and `regexConstructor`, see [Shiki - RegExp Engines](https://shiki.style/guide/regex-engines) for details.
188
+
189
+ The JavaScript Raw engine works the same way, but only with [pre-compiled languages](https://shiki.style/guide/regex-engines#pre-compiled-languages) (`@shikijs/langs-precompiled`). The full and web bundles don't support the raw engine because they bundle standard grammars, you need a [custom highlighter](#custom-highlighters-core-bundle).
179
190
 
180
- When using the core bundle, you must specify an engine:
191
+ > [!IMPORTANT]
192
+ > **Heads up:** in a future release, react-shiki's default engine will be swapped from Oniguruma WASM to the JavaScript engine (with [`forgiving`](https://shiki.style/guide/regex-engines#use-with-unsupported-languages) enabled by default).
193
+
194
+ #### Custom Highlighters (Core Bundle)
195
+
196
+ Named engines (`'javascript'`, `'oniguruma'`) don't apply to the core bundle since you create the highlighter with `createHighlighterCore`; instead, use `createJavaScriptRegexEngine` or `createOnigurumaEngine` to create a custom engine instance:
181
197
 
182
198
  ```tsx
183
199
  import {
@@ -193,42 +209,130 @@ const highlighter = await createHighlighterCore({
193
209
  });
194
210
  ```
195
211
 
196
- #### Engine Options
212
+ ##### JavaScript Raw Engine
197
213
 
198
- 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:
214
+ The JavaScript Raw engine is the smallest option at only ~1 KB, but requires [pre-compiled languages](https://shiki.style/guide/regex-engines#pre-compiled-languages), and faces a known upstream bug -- [shikijs/shiki:issue#918](https://github.com/shikijs/shiki/issues/918) -- causing some languages (Python, HTML, Perl, and YAML among those reported) to highlight incorrectly.
199
215
 
200
216
  ```tsx
201
- createJavaScriptRegexEngine({ forgiving: true });
217
+ import {
218
+ useShikiHighlighter,
219
+ createHighlighterCore,
220
+ createJavaScriptRawEngine,
221
+ } from 'react-shiki/core';
222
+
223
+ // Module scope: raw engine + pre-compiled grammars, smallest and fastest
224
+ const highlighter = await createHighlighterCore({
225
+ themes: [import('@shikijs/themes/github-dark')],
226
+ langs: [import('@shikijs/langs-precompiled/typescript')],
227
+ engine: createJavaScriptRawEngine()
228
+ });
229
+
230
+ const highlightedCode = useShikiHighlighter(code, 'typescript', 'github-dark', {
231
+ highlighter
232
+ });
202
233
  ```
203
234
 
204
- See [Shiki - RegExp Engines](https://shiki.style/guide/regex-engines) for more info.
235
+ Be sure to verify your language highlights correctly if you choose to use the raw engine ([shikijs/shiki:issue#918](https://github.com/shikijs/shiki/issues/918)).
236
+
237
+
238
+ ## Output Formats
205
239
 
240
+ `react-shiki` can return highlighted code in three formats:
241
+
242
+ **React Nodes (Default)** - Rendered as React elements, no `dangerouslySetInnerHTML` required
243
+ ```tsx
244
+ // Hook
245
+ const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark");
246
+
247
+ // Component
248
+ <ShikiHighlighter language="tsx" theme="github-dark">
249
+ {code}
250
+ </ShikiHighlighter>
251
+ ```
252
+
253
+ **HTML String** - Rendered via `dangerouslySetInnerHTML`
254
+ ```tsx
255
+ // Hook (returns HTML string, use dangerouslySetInnerHTML to render)
256
+ const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
257
+ outputFormat: 'html'
258
+ });
259
+
260
+ // Component (automatically uses dangerouslySetInnerHTML when outputFormat is 'html')
261
+ <ShikiHighlighter language="tsx" theme="github-dark" outputFormat="html">
262
+ {code}
263
+ </ShikiHighlighter>
264
+ ```
265
+
266
+ **Shiki Tokens (Experimental)** - Hook-only output (bring your own renderer, react-shiki does not provide one)
267
+ ```tsx
268
+ const highlighted = useShikiHighlighter(code, "tsx", "github-dark", {
269
+ outputFormat: "tokens",
270
+ });
271
+
272
+ // Sample only, not a production renderer: you own rendering, memoization,
273
+ // and commit management (critical for streaming highlighting)
274
+ const rendered = highlighted?.tokens.map((line, i) => (
275
+ <div key={i}>
276
+ {line.map((token, j) => (
277
+ <span key={j} style={{ color: token.color }}>
278
+ {token.content}
279
+ </span>
280
+ ))}
281
+ </div>
282
+ ));
283
+ ```
284
+
285
+ The React and HTML formats spend their rendering work in different phases. HTML output skips per-token React element creation and reconciliation, reducing render-phase overhead for large, one-shot highlights, but each update replaces the code block's entire DOM subtree via `innerHTML`. React output pays for element creation and diffing on every update, but commits only incremental DOM mutations, minimizing DOM churn for frequently re-highlighted code such as streaming LLM output.
286
+
287
+ HTML output hands the highlighted markup to the DOM via `dangerouslySetInnerHTML`, so only use it with trusted code sources. The default React output is the safe choice for untrusted content.
288
+
289
+ Token output is for when you need to own rendering yourself; it is intentionally available on the hook, not the `ShikiHighlighter` component, since react-shiki's markup-based features no longer apply once you control the markup. Note that Shiki's `codeToTokens` does not run transformers or decorations, so markup-producing options, including transformers, decorations, line numbers, and `structure`, have no effect on token output.
206
290
 
207
291
  ## Configuration
208
292
 
209
293
  ### Common Configuration Options
210
294
 
295
+ The core inputs, positional arguments on the hook, props/children on the component:
211
296
 
212
- | Option | Type | Default | Description |
297
+ | Input | Type | Default | Description |
213
298
  | ------------------- | ------------------ | --------------- | ----------------------------------------------------------------------------- |
214
299
  | `code` | `string` | - | Code to highlight |
215
- | `language` | `string \| object` | - | Language to highlight, built-in or custom textmate grammer object |
216
- | `theme` | `string \| object` | `'github-dark'` | Single or multi-theme configuration, built-in or custom textmate theme object |
217
- | `delay` | `number` | `0` | Delay between highlights (in milliseconds) |
300
+ | `language` | `string \| object` | - | Language to highlight, built-in or custom TextMate grammar object |
301
+ | `theme` | `string \| object` | `'github-dark'` | Single or multi-theme configuration, built-in or custom TextMate theme object |
302
+
303
+ #### react-shiki Options
304
+
305
+
306
+ | Option | Type | Default | Description |
307
+ | ------------------- | ------------------ | --------------- | ----------------------------------------------------------------------------- |
308
+ | `delay` | `number` | - | Minimum time between highlights (in milliseconds), no throttling by default |
309
+ | `engine` | `'javascript' \| 'oniguruma' \| RegexEngine` | `'oniguruma'` | RegExp engine for syntax highlighting; named engines are created and cached internally |
310
+ | `highlighter` | `Highlighter \| HighlighterCore` | - | Custom highlighter instance, required for the core bundle |
311
+ | `outputFormat` | `string` | `'react'` | Output format: 'react' for React nodes, 'html' for HTML string, or 'tokens' for Shiki tokens (hook only, experimental) |
312
+ | `preloadLanguages` | `array` | `[]` | Preload bundled language IDs, custom grammar objects, or dynamic grammar imports |
218
313
  | `customLanguages` | `array` | `[]` | **Deprecated**: use `preloadLanguages` instead. |
219
- | `preloadLanguages` | `array` | `[]` | Preload bundled language IDs and custom language grammars |
220
- | `langAlias` | `object` | `{}` | Map of language aliases |
221
- | `engine` | `RegexEngine` | Oniguruma | RegExp engine for syntax highlighting (Oniguruma, JavaScript RegExp, or JavaScript Raw) |
222
314
  | `showLineNumbers` | `boolean` | `false` | Display line numbers alongside code |
223
315
  | `startingLineNumber` | `number` | `1` | Starting line number when line numbers are enabled |
224
- | `transformers` | `array` | `[]` | Custom Shiki transformers for modifying the highlighting output |
225
- | `cssVariablePrefix` | `string` | `'--shiki'` | Prefix for CSS variables storing theme colors |
226
- | `defaultColor` | `string \| false` | `'light'` | Default theme mode when using multiple themes, can also disable default theme |
227
- | `outputFormat` | `string` | `'react'` | Output format: 'react' for React nodes, 'html' for HTML string |
228
- | `tabindex` | `number` | `0` | Tab index for the code block |
229
- | `decorations` | `array` | `[]` | Custom decorations to wrap the highlighted tokens with |
230
- | `structure` | `string` | `classic` | The structure of the generated HAST and HTML - `classic` or `inline` |
231
- | [`codeToHastOptions`](https://github.com/shikijs/shiki/blob/main/packages/types/src/options.ts#L121) | - | - | All other options supported by Shiki's `codeToHast` |
316
+ | `highlightLineNumbers` | `number[]` | - | Displayed line numbers to highlight |
317
+
318
+ #### Shiki Pass-through Options
319
+
320
+ All of Shiki's options pass-through and are accessible in react-shiki.
321
+
322
+ Based on `outputFormat`:
323
+ - `'tokens'`: All options supported by Shiki's [`codeToTokens`](https://github.com/shikijs/shiki/blob/v4.3.0/packages/types/src/tokens.ts#L27)
324
+ - `'html'`: All options supported by Shiki's [`codeToHtml`](https://github.com/shikijs/shiki/blob/v4.3.0/packages/types/src/options.ts#L136)
325
+ - `'react'`: All options supported by Shiki's [`codeToHast`](https://github.com/shikijs/shiki/blob/v4.3.0/packages/types/src/options.ts#L136)
326
+
327
+ Commonly used:
328
+ - `transformers`: modify the highlighting output with Shiki transformers (default: `[]`)
329
+ - `decorations`: wrap ranges of highlighted tokens (default: `[]`)
330
+ - `defaultColor`: default theme mode with multi-theme, `false` to disable (default: `'light'`)
331
+ - `cssVariablePrefix`: prefix for theme color CSS variables (default: `'--shiki-'`)
332
+ - `langAlias`: map custom language names to bundled ones (default: `{}`)
333
+ - `structure`: `'classic'` or `'inline'` HAST/HTML shape (default: `'classic'`)
334
+ - `tabindex`: tab index of the code block (default: `0`)
335
+ - `meta`: arbitrary metadata passed to transformers (default: `{}`)
232
336
 
233
337
  ### Component-specific Props
234
338
 
@@ -246,7 +350,7 @@ The `ShikiHighlighter` component offers minimal built-in styling and customizati
246
350
 
247
351
  ### Embedded Language Highlighting
248
352
 
249
- 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.
353
+ react-shiki uses Shiki's [`guessEmbeddedLanguages`](https://github.com/shikijs/shiki/blob/main/packages/core/src/utils/strings.ts) 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.
250
354
 
251
355
  ### Multi-theme Support
252
356
 
@@ -333,11 +437,11 @@ Ensure your site sets the `color-scheme` CSS property:
333
437
 
334
438
  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.
335
439
 
336
- > **Note**: The `light-dark()` function requires modern browser support. For older browsers, use the manual CSS variables approach.
440
+ > **Note**: The `light-dark()` function requires [modern browser support](https://caniuse.com/wf-light-dark). For older browsers, use the manual CSS variables approach.
337
441
 
338
442
  ### Custom Themes
339
443
 
340
- 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).
444
+ Custom themes can be passed as a TextMate theme JavaScript object ([example](https://github.com/antfu/textmate-grammars-themes/blob/main/packages/tm-themes/themes/dark-plus.json)).
341
445
 
342
446
  ```tsx
343
447
  import tokyoNight from "../styles/tokyo-night.json";
@@ -351,9 +455,29 @@ import tokyoNight from "../styles/tokyo-night.json";
351
455
  const highlightedCode = useShikiHighlighter(code, "tsx", tokyoNight);
352
456
  ```
353
457
 
458
+ When using a fine-grained highlighter via `react-shiki/core`: pass the theme as a dynamic import to `createHighlighterCore` so it stays out of the initial chunk, then reference it by its `name` field:
459
+
460
+ ```tsx
461
+ const highlighter = await createHighlighterCore({
462
+ themes: [import("../styles/tokyo-night.json")],
463
+ langs: [import("@shikijs/langs/tsx")],
464
+ engine: createJavaScriptRegexEngine(),
465
+ });
466
+
467
+ // Component
468
+ <ShikiHighlighter highlighter={highlighter} language="tsx" theme="tokyo-night">
469
+ {code.trim()}
470
+ </ShikiHighlighter>
471
+
472
+ // Hook
473
+ const highlightedCode = useShikiHighlighter(code, "tsx", tokyoNight, {
474
+ highlighter,
475
+ });
476
+ ```
477
+
354
478
  ### Custom Languages
355
479
 
356
- 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)
480
+ Custom languages should be passed as a TextMate grammar JavaScript object ([example](https://github.com/shikijs/textmate-grammars-themes/blob/main/packages/tm-grammars/grammars/typescript.json)).
357
481
  ```tsx
358
482
  import mcfunction from "../langs/mcfunction.tmLanguage.json";
359
483
 
@@ -366,6 +490,28 @@ import mcfunction from "../langs/mcfunction.tmLanguage.json";
366
490
  const highlightedCode = useShikiHighlighter(code, mcfunction, "github-dark");
367
491
  ```
368
492
 
493
+ Passing grammar objects as props works with the full and web bundles, where react-shiki creates the highlighter and loads them for you. To keep custom grammars out of the main bundle, pass them as dynamic imports via `preloadLanguages` and reference them by name — see [Preloading Languages](#preloading-languages).
494
+
495
+ On the core bundle you own the highlighter, so include grammars there as dynamic imports:
496
+
497
+ ```tsx
498
+ const highlighter = await createHighlighterCore({
499
+ themes: [import("@shikijs/themes/github-dark")],
500
+ langs: [import("../langs/mcfunction.tmLanguage.json")],
501
+ engine: createJavaScriptRegexEngine(),
502
+ });
503
+
504
+ // Component
505
+ <ShikiHighlighter highlighter={highlighter} language="mcfunction" theme="github-dark">
506
+ {code.trim()}
507
+ </ShikiHighlighter>
508
+
509
+ // Hook
510
+ const highlightedCode = useShikiHighlighter(code, "mcfunction", "github-dark", {
511
+ highlighter,
512
+ });
513
+ ```
514
+
369
515
  #### Preloading Languages
370
516
 
371
517
  If highlighting is dynamic at runtime, preload custom languages.
@@ -389,8 +535,29 @@ const highlightedCode = useShikiHighlighter(code, "typescript", "github-dark", {
389
535
  });
390
536
  ```
391
537
 
538
+ Custom grammars can also be dynamic imports, keeping them out of the main bundle. Reference them by the grammar's `name` field:
539
+
540
+ ```tsx
541
+ // Module scope: stable identity across renders, fetched on first highlight
542
+ const preloadLanguages = [
543
+ () => import("../langs/mcfunction.tmLanguage.json"),
544
+ () => import("../langs/bosque.tmLanguage.json"),
545
+ ];
546
+
547
+ <ShikiHighlighter
548
+ language="mcfunction"
549
+ theme="github-dark"
550
+ preloadLanguages={preloadLanguages}
551
+ >
552
+ {code.trim()}
553
+ </ShikiHighlighter>
554
+ ```
555
+
556
+ > [!IMPORTANT]
557
+ > Define dynamic imports at module scope, preferring the getter form (`() => import(...)`). A fresh arrow function on every render re-triggers highlighting; bare promises are compared by type, so swapping one at runtime won't be picked up.
558
+
392
559
  > [!NOTE]
393
- > Bundled language are dynamically loaded as needed, and do not need to be preloaded as long as they exist in your bundle.
560
+ > Bundled languages are loaded on demand and do not need to be preloaded. `preloadLanguages` applies to the full and web bundles; with a custom highlighter, load languages in `createHighlighterCore` instead.
394
561
 
395
562
  ### Language Aliases
396
563
 
@@ -430,47 +597,37 @@ const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
430
597
 
431
598
  ### Line Numbers
432
599
 
433
- Display line numbers alongside your code, these are CSS-based
434
- and can be customized with CSS variables:
600
+ Line numbers are CSS-based and can be customized with CSS variables:
435
601
 
436
602
  ```tsx
437
603
  // Component
438
604
  <ShikiHighlighter
439
605
  language="javascript"
440
606
  theme="github-dark"
441
- showLineNumbers,
607
+ showLineNumbers
442
608
  startingLineNumber={0} // default is 1
609
+ highlightLineNumbers={[2, 4]}
443
610
  >
444
611
  {code}
445
612
  </ShikiHighlighter>
446
613
 
447
- <ShikiHighlighter
448
- language="python"
449
- theme="github-dark"
450
- showLineNumbers
451
- startingLineNumber={0}
452
- >
453
- {code}
454
- </ShikiHighlighter>
455
-
456
- // Hook (import 'react-shiki/css' for line numbers to work)
614
+ // Hook (line-number styles inject automatically when enabled)
457
615
  const highlightedCode = useShikiHighlighter(code, "javascript", "github-dark", {
458
616
  showLineNumbers: true,
459
- startingLineNumber: 0,
617
+ startingLineNumber: 0,
618
+ highlightLineNumbers: [2, 4],
460
619
  });
461
620
  ```
462
621
 
463
622
  > [!NOTE]
464
- > When using the hook with line numbers, import the CSS file or provide your own CSS
465
- > for `.rs-line-number` (line `span`) and `.rs-has-line-numbers` (container `code` element). <!-- TODO: The CSS vars/classes/selectors are poorly documented -->
466
- > ```tsx
467
- > import 'react-shiki/css';
468
- > ```
623
+ > No CSS import is needed: the shared highlighting hook injects line-number and highlight styles automatically when those features are enabled, including when used through the component.
624
+ > To customize them, override `.rs-line-number` (line `span`), `.rs-highlighted-line` (line `span`), and `.rs-has-line-numbers` / `.rs-has-highlighted-lines` (container `code` element) in your own CSS.
469
625
 
470
- > [!WARNING]
471
- > Legacy `.line-numbers` / `.has-line-numbers` selectors and unprefixed line-number CSS variables are removed as of 0.10.1.
626
+ `highlightLineNumbers` uses displayed line numbers, so it works with `startingLineNumber`.
472
627
 
473
- Component-internal default classes are namespaced under `rs-*` and shipped inside `@layer base` so app-level utilities can override them more predictably.
628
+ Component styles are embedded in the JS and installed automatically when the component mounts — no CSS import or bundler configuration needed. Injection is scoped to usage: component styles tree-shake out of hook-only bundles entirely, while line-number/highlight styles are installed only when those options are enabled. The complete compiled stylesheet is also exported as `react-shiki/css` for build-time CSS pipelines or as an externally loaded fallback. Importing it does not disable automatic runtime injection.
629
+
630
+ Component-internal default classes are namespaced under `rs-*` and written with zero-specificity `:where()` selectors, so they can't be clobbered by CSS resets (e.g. Tailwind preflight) regardless of stylesheet load order, while any rule in your own CSS — even a bare element selector — overrides them.
474
631
 
475
632
  Available CSS variables for customization:
476
633
  ```css
@@ -481,8 +638,12 @@ Available CSS variables for customization:
481
638
  --rs-line-numbers-font-size: inherit;
482
639
  --rs-line-numbers-font-weight: inherit;
483
640
  --rs-line-numbers-opacity: 1;
641
+ --rs-highlighted-line-background: color-mix(in srgb, currentColor 10%, transparent);
642
+ --rs-highlighted-line-number-foreground: color-mix(in srgb, currentColor 65%, transparent);
484
643
  ```
485
644
 
645
+ With default styles, highlighted-line backgrounds automatically extend into the `pre`'s horizontal padding, reaching the container edges. When styling the `pre` yourself, highlights cover the code content area.
646
+
486
647
  You can customize them in your own CSS or by using the style prop on the component:
487
648
  ```tsx
488
649
  <ShikiHighlighter
@@ -573,9 +734,9 @@ const CodeHighlight = ({ className, children, node, ...props }) => {
573
734
  **Method 2: Using the `rehypeInlineCodeProperty` plugin:**
574
735
 
575
736
  `react-shiki` also exports `rehypeInlineCodeProperty`, a rehype plugin that
576
- provides the same API as `react-markdown` prior to `9.0.0`. It reintroduces the
577
- `inline` prop which works by checking if `<code>` is nested within a `<pre>` tag,
578
- if not, it's considered inline code and the `inline` prop is set to `true`.
737
+ adds an `inline` prop to `react-markdown` code components. It works by checking
738
+ if `<code>` is nested within a `<pre>` tag; if not, it's considered inline code
739
+ and the `inline` prop is set to `true`.
579
740
 
580
741
  It's passed as a `rehypePlugin` to `react-markdown`:
581
742
 
@@ -623,7 +784,7 @@ const CodeHighlight = ({
623
784
 
624
785
  ### Throttling Real-time Highlighting
625
786
 
626
- For improved performance when highlighting frequently changing code:
787
+ To reduce highlighting work for frequently changing code:
627
788
 
628
789
  ```tsx
629
790
  // With the component
@@ -637,36 +798,6 @@ const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
637
798
  });
638
799
  ```
639
800
 
640
- ### Output Format Optimization
641
-
642
- `react-shiki` provides two output formats to balance safety and performance:
643
-
644
- **React Nodes (Default)** - Safer, no `dangerouslySetInnerHTML` required
645
- ```tsx
646
- // Hook
647
- const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark");
648
-
649
- // Component
650
- <ShikiHighlighter language="tsx" theme="github-dark">
651
- {code}
652
- </ShikiHighlighter>
653
- ```
654
-
655
- **HTML String** - 15-45% faster performance
656
- ```tsx
657
- // Hook (returns HTML string, use dangerouslySetInnerHTML to render)
658
- const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
659
- outputFormat: 'html'
660
- });
661
-
662
- // Component (automatically uses dangerouslySetInnerHTML when outputFormat is 'html')
663
- <ShikiHighlighter language="tsx" theme="github-dark" outputFormat="html">
664
- {code}
665
- </ShikiHighlighter>
666
- ```
667
-
668
- 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.
669
-
670
801
  ---
671
802
 
672
803
  Made with ❤️ by [Bassim (AVGVSTVS96)](https://github.com/AVGVSTVS96)