Skip to content

Commit 5fa19ee

Browse files
authored
feat: Add outputFormat option, support HTML in addition to JSX output (#92)
Conversion to React nodes can be 15-45% slower than passing HTML strings to `dangerouslySetInnerHtml`. This option adds support for HTML output, When the `outputFormat is set to HTML, the component will automatically pass output to `dangerouslySetInnerHtml`. When using the hook, you handle the output directly.
1 parent b57685d commit 5fa19ee

24 files changed

+1040
-1361
lines changed

.changeset/bright-moments-hear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-shiki": minor
3+
---
4+
5+
Add `outputFormat` option, support HTML in addition to JSX output for improved performance

package/README.md

Lines changed: 40 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,46 @@
11
# 🎨 [react-shiki](https://npmjs.com/react-shiki)
22

3-
> [!NOTE]
4-
> This library is still in development. More features will be implemented, and the API may change.
5-
> Contributions are welcome!
63

74
A performant client-side syntax highlighting component and hook for React, built with [Shiki](https://shiki.matsu.io/).
85

96
[See the demo page with highlighted code blocks showcasing several Shiki themes!](https://react-shiki.vercel.app/)
107

118
<!--toc:start-->
12-
139
- 🎨 [react-shiki](https://npmjs.com/react-shiki)
1410
- [Features](#features)
1511
- [Installation](#installation)
1612
- [Usage](#usage)
1713
- [Bundle Options](#bundle-options)
14+
- [`react-shiki` (Full Bundle)](#react-shiki-full-bundle)
15+
- [`react-shiki/web` (Web Bundle)](#react-shikiweb-web-bundle)
16+
- [`react-shiki/core` (Minimal Bundle)](#react-shikicore-minimal-bundle)
17+
- [RegExp Engines](#regexp-engines)
1818
- [Configuration](#configuration)
1919
- [Common Configuration Options](#common-configuration-options)
2020
- [Component-specific Props](#component-specific-props)
2121
- [Multi-theme Support](#multi-theme-support)
22+
- [Making Themes Reactive](#making-themes-reactive)
23+
- [Option 1: Using `light-dark()` Function (Recommended)](#option-1-using-light-dark-function-recommended)
24+
- [Option 2: CSS Theme Switching](#option-2-css-theme-switching)
2225
- [Custom Themes](#custom-themes)
2326
- [Custom Languages](#custom-languages)
2427
- [Preloading Custom Languages](#preloading-custom-languages)
28+
- [Language Aliases](#language-aliases)
2529
- [Custom Transformers](#custom-transformers)
2630
- [Line Numbers](#line-numbers)
2731
- [Integration](#integration)
2832
- [Integration with react-markdown](#integration-with-react-markdown)
2933
- [Handling Inline Code](#handling-inline-code)
3034
- [Performance](#performance)
3135
- [Throttling Real-time Highlighting](#throttling-real-time-highlighting)
36+
- [Output Format Optimization](#output-format-optimization)
3237
- [Streaming and LLM Chat UI](#streaming-and-llm-chat-ui)
33-
<!--toc:end-->
38+
<!--toc:end-->
3439

3540
## Features
3641

3742
- 🖼️ Provides both a `ShikiHighlighter` component and a `useShikiHighlighter` hook for more flexibility
38-
- 🔐 Shiki output is processed from HAST directly into React elements, no `dangerouslySetInnerHTML` required
43+
- 🔐 Flexible output: Choose between React elements (no `dangerouslySetInnerHTML`) or HTML strings for better performance
3944
- 📦 Multiple bundle options: Full bundle (~1.2MB gz), web bundle (~695KB gz), or minimal core bundle for fine-grained bundle control
4045
- 🖌️ Full support for custom TextMate themes and languages
4146
- 🔧 Supports passing custom Shiki transformers to the highlighter, in addition to all other options supported by `codeToHast`
@@ -90,7 +95,7 @@ function CodeBlock({ code, language }) {
9095
```tsx
9196
import ShikiHighlighter from 'react-shiki';
9297
```
93-
- **Size**: ~6.4MB minified, 1.2MB gzipped
98+
- **Size**: ~6.4MB minified, ~1.2MB gzipped (includes ~12KB react-shiki)
9499
- **Languages**: All Shiki languages and themes
95100
- **Use case**: Unknown language requirements, maximum language support
96101
- **Setup**: Zero configuration required
@@ -99,7 +104,7 @@ import ShikiHighlighter from 'react-shiki';
99104
```tsx
100105
import ShikiHighlighter from 'react-shiki/web';
101106
```
102-
- **Size**: ~3.8MB minified, 695KB gzipped
107+
- **Size**: ~3.8MB minified, ~707KB gzipped (includes ~12KB react-shiki)
103108
- **Languages**: Web-focused languages (HTML, CSS, JS, TS, JSON, Markdown, Vue, JSX, Svelte)
104109
- **Use case**: Web applications with balanced size/functionality
105110
- **Setup**: Drop-in replacement for main entry point
@@ -124,7 +129,7 @@ const highlighter = await createHighlighterCore({
124129
{code}
125130
</ShikiHighlighter>
126131
```
127-
- **Size**: Minimal (only what you import)
132+
- **Size**: ~12KB + your imported themes/languages
128133
- **Languages**: User-defined via custom highlighter
129134
- **Use case**: Production apps requiring maximum bundle control
130135
- **Setup**: Requires custom highlighter configuration
@@ -163,6 +168,7 @@ See [Shiki - RegExp Engines](https://shiki.style/guide/regex-engines) for more i
163168
| `transformers` | `array` | `[]` | Custom Shiki transformers for modifying the highlighting output |
164169
| `cssVariablePrefix` | `string` | `'--shiki'` | Prefix for CSS variables storing theme colors |
165170
| `defaultColor` | `string \| false` | `'light'` | Default theme mode when using multiple themes, can also disable default theme |
171+
| `outputFormat` | `string` | `'react'` | Output format: 'react' for React nodes, 'html' for HTML string |
166172
| `tabindex` | `number` | `0` | Tab index for the code block |
167173
| `decorations` | `array` | `[]` | Custom decorations to wrap the highlighted tokens with |
168174
| `structure` | `string` | `classic` | The structure of the generated HAST and HTML - `classic` or `inline` |
@@ -253,12 +259,12 @@ Ensure your site sets the `color-scheme` CSS property:
253259
color-scheme: light dark;
254260
}
255261

256-
/* Or dynamically with a class */
257-
* {
262+
/* Or dynamically for class based dark mode */
263+
:root {
258264
color-scheme: light;
259265
}
260266

261-
.dark {
267+
:root.dark {
262268
color-scheme: dark;
263269
}
264270
```
@@ -564,121 +570,36 @@ const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
564570
});
565571
```
566572

567-
### Streaming and LLM Chat UI
568-
569-
`react-shiki` can be used to highlight streamed code from LLM responses in real-time.
573+
### Output Format Optimization
570574

571-
I use it for an LLM chatbot UI, it renders markdown and highlights
572-
code in memoized chat messages.
573-
574-
Using `useShikiHighlighter` hook:
575+
`react-shiki` provides two output formats to balance safety and performance:
575576

577+
**React Nodes (Default)** - Safer, no `dangerouslySetInnerHTML` required
576578
```tsx
577-
import type { ReactNode } from "react";
578-
import { isInlineCode, useShikiHighlighter, type Element } from "react-shiki";
579-
import tokyoNight from "@styles/tokyo-night.mjs";
580-
581-
interface CodeHighlightProps {
582-
className?: string | undefined;
583-
children?: ReactNode | undefined;
584-
node?: Element | undefined;
585-
}
586-
587-
export const CodeHighlight = ({
588-
className,
589-
children,
590-
node,
591-
...props
592-
}: CodeHighlightProps) => {
593-
const code = String(children).trim();
594-
const language = className?.match(/language-(\w+)/)?.[1];
595-
596-
const isInline = node ? isInlineCode(node) : false;
597-
598-
const highlightedCode = useShikiHighlighter(code, language, tokyoNight, {
599-
delay: 150,
600-
});
579+
// Hook
580+
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark");
601581

602-
return !isInline ? (
603-
<div
604-
className="shiki not-prose relative [&_pre]:overflow-auto
605-
[&_pre]:rounded-lg [&_pre]:px-6 [&_pre]:py-5"
606-
>
607-
{language ? (
608-
<span
609-
className="absolute right-3 top-2 text-xs tracking-tighter
610-
text-muted-foreground/85"
611-
>
612-
{language}
613-
</span>
614-
) : null}
615-
{highlightedCode}
616-
</div>
617-
) : (
618-
<code className={className} {...props}>
619-
{children}
620-
</code>
621-
);
622-
};
582+
// Component
583+
<ShikiHighlighter language="tsx" theme="github-dark">
584+
{code}
585+
</ShikiHighlighter>
623586
```
624587

625-
Or using the `ShikiHighlighter` component:
626-
588+
**HTML String** - 15-45% faster performance
627589
```tsx
628-
import type { ReactNode } from "react";
629-
import ShikiHighlighter, { isInlineCode, type Element } from "react-shiki";
630-
631-
interface CodeHighlightProps {
632-
className?: string | undefined;
633-
children?: ReactNode | undefined;
634-
node?: Element | undefined;
635-
}
636-
637-
export const CodeHighlight = ({
638-
className,
639-
children,
640-
node,
641-
...props
642-
}: CodeHighlightProps): JSX.Element => {
643-
const match = className?.match(/language-(\w+)/);
644-
const language = match ? match[1] : undefined;
645-
const code = String(children).trim();
646-
647-
const isInline: boolean | undefined = node ? isInlineCode(node) : undefined;
590+
// Hook (returns HTML string, use dangerouslySetInnerHTML to render)
591+
const highlightedCode = useShikiHighlighter(code, "tsx", "github-dark", {
592+
outputFormat: 'html'
593+
});
648594

649-
return !isInline ? (
650-
<ShikiHighlighter
651-
language={language}
652-
theme="github-dark"
653-
delay={150}
654-
{...props}
655-
>
656-
{code}
657-
</ShikiHighlighter>
658-
) : (
659-
<code className={className}>{code}</code>
660-
);
661-
};
595+
// Component (automatically uses dangerouslySetInnerHTML when outputFormat is 'html')
596+
<ShikiHighlighter language="tsx" theme="github-dark" outputFormat="html">
597+
{code}
598+
</ShikiHighlighter>
662599
```
663600

664-
Passed to `react-markdown` as a `code` component in memoized chat messages:
601+
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.
665602

666-
```tsx
667-
const RenderedMessage = React.memo(({ message }: { message: Message }) => (
668-
<div className={cn(messageStyles[message.role])}>
669-
<ReactMarkdown components={{ code: CodeHighlight }}>
670-
{message.content}
671-
</ReactMarkdown>
672-
</div>
673-
));
674-
675-
export const ChatMessages = ({ messages }: { messages: Message[] }) => {
676-
return (
677-
<div className="space-y-4">
678-
{messages.map((message) => (
679-
<RenderedMessage key={message.id} message={message} />
680-
))}
681-
</div>
682-
);
683-
};
684-
```
603+
---
604+
605+
Made with ❤️ by [Bassim (AVGVSTVS96)](https://github.com/AVGVSTVS96)

package/package.json

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,8 @@
2626
"type": "module",
2727
"main": "./dist/index.js",
2828
"types": "./dist/index.d.ts",
29-
"files": [
30-
"dist",
31-
"src/lib/styles.css"
32-
],
33-
"sideEffects": [
34-
"src/lib/styles.css"
35-
],
29+
"files": ["dist", "src/lib/styles.css"],
30+
"sideEffects": ["src/lib/styles.css"],
3631
"exports": {
3732
".": {
3833
"types": "./dist/index.d.ts",
@@ -87,7 +82,6 @@
8782
"@types/node": "22.17.2",
8883
"@types/react": "catalog:",
8984
"@vitejs/plugin-react": "^4.7.0",
90-
"benny": "^3.7.1",
9185
"html-react-parser": "^5.2.6",
9286
"jsdom": "^26.1.0",
9387
"tsup": "^8.5.0",

0 commit comments

Comments
 (0)