Skip to content

Commit 44d670a

Browse files
authored
fix: component multi-theme support (#87)
fix: component multi-theme support fixes issue where multi-theme support doesn't work when used with the component add testing for component update readme with light-dark() support fix: react types
1 parent 035301b commit 44d670a

File tree

6 files changed

+227
-71
lines changed

6 files changed

+227
-71
lines changed

.changeset/mean-boats-tap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-shiki": patch
3+
---
4+
5+
fix multi-theme support in component

package/README.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,57 @@ const highlightedCode = useShikiHighlighter(
217217
);
218218
```
219219

220-
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.
220+
#### Making Themes Reactive
221+
222+
There are two approaches to make multi-themes reactive to your site's theme:
223+
224+
##### Option 1: Using `light-dark()` Function (Recommended)
225+
226+
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:
227+
228+
```tsx
229+
// Component
230+
<ShikiHighlighter
231+
language="tsx"
232+
theme={{
233+
light: "github-light",
234+
dark: "github-dark",
235+
}}
236+
defaultColor="light-dark()"
237+
>
238+
{code.trim()}
239+
</ShikiHighlighter>
240+
241+
// Hook
242+
const highlightedCode = useShikiHighlighter(code, "tsx", {
243+
light: "github-light",
244+
dark: "github-dark",
245+
}, {
246+
defaultColor: "light-dark()"
247+
});
248+
```
249+
250+
Ensure your site sets the `color-scheme` CSS property:
251+
```css
252+
:root {
253+
color-scheme: light dark;
254+
}
255+
256+
/* Or dynamically with a class */
257+
* {
258+
color-scheme: light;
259+
}
260+
261+
.dark {
262+
color-scheme: dark;
263+
}
264+
```
265+
266+
##### Option 2: CSS Theme Switching
267+
268+
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.
269+
270+
> **Note**: The `light-dark()` function requires modern browser support. For older browsers, use the manual CSS variables approach.
221271
222272
### Custom Themes
223273

package/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,19 @@
5959
"check": "tsc && biome check ."
6060
},
6161
"peerDependencies": {
62+
"@types/react": ">=16.8.0",
63+
"@types/react-dom": ">=16.8.0",
6264
"react": ">= 16.8.0",
6365
"react-dom": ">= 16.8.0"
6466
},
67+
"peerDependenciesMeta": {
68+
"@types/react": {
69+
"optional": true
70+
},
71+
"@types/react-dom": {
72+
"optional": true
73+
}
74+
},
6575
"dependencies": {
6676
"clsx": "^2.1.1",
6777
"dequal": "^2.0.3",

package/src/__tests__/multi-theme.test.tsx

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { render, waitFor } from '@testing-library/react';
3-
import { useShikiHighlighter } from '../index';
3+
import { useShikiHighlighter, ShikiHighlighter } from '../index';
44

55
// Test component with configurable options
66
const TestComponent = ({
@@ -106,3 +106,84 @@ describe('Multi-theme support', () => {
106106
});
107107
});
108108
});
109+
110+
describe('ShikiHighlighter Component Multi-theme support', () => {
111+
const code = 'console.log("test");';
112+
const language = 'javascript';
113+
const themes = { light: 'github-light', dark: 'github-dark' };
114+
115+
test('component with multi-themes should render CSS variables for non-default theme', async () => {
116+
const { container } = render(
117+
<ShikiHighlighter language={language} theme={themes}>
118+
{code}
119+
</ShikiHighlighter>
120+
);
121+
122+
await waitFor(() => {
123+
const shikiContainer = container.querySelector('[data-testid="shiki-container"]');
124+
expect(shikiContainer).toBeInTheDocument();
125+
126+
const pre = shikiContainer?.querySelector('pre');
127+
expect(pre).toBeInTheDocument();
128+
expect(pre).toHaveClass('shiki');
129+
130+
// Find spans with CSS variables
131+
const spans = container.querySelectorAll('span[style*="--shiki-"]');
132+
expect(spans.length).toBeGreaterThan(0);
133+
134+
// When no defaultColor is specified, light is default, so we should have --shiki-dark
135+
const span = spans[0];
136+
const style = span?.getAttribute('style');
137+
expect(style).toContain('--shiki-dark');
138+
expect(style).not.toContain('--shiki-light'); // light is the default, so no CSS variable
139+
});
140+
});
141+
142+
test('component with multi-themes and defaultColor=dark should show light variables', async () => {
143+
const { container } = render(
144+
<ShikiHighlighter
145+
language={language}
146+
theme={themes}
147+
defaultColor="dark"
148+
>
149+
{code}
150+
</ShikiHighlighter>
151+
);
152+
153+
await waitFor(() => {
154+
const spans = container.querySelectorAll('span[style*="--shiki-"]');
155+
expect(spans.length).toBeGreaterThan(0);
156+
157+
// When defaultColor=dark, dark is default, so we should have --shiki-light
158+
const span = spans[0];
159+
const style = span?.getAttribute('style');
160+
expect(style).toContain('--shiki-light');
161+
expect(style).not.toContain('--shiki-dark'); // dark is the default, so no CSS variable
162+
});
163+
});
164+
165+
test('component with multi-themes and custom cssVariablePrefix should work', async () => {
166+
const { container } = render(
167+
<ShikiHighlighter
168+
language={language}
169+
theme={themes}
170+
cssVariablePrefix="--custom-"
171+
defaultColor="light"
172+
>
173+
{code}
174+
</ShikiHighlighter>
175+
);
176+
177+
await waitFor(() => {
178+
const spans = container.querySelectorAll('span[style*="--custom-"]');
179+
expect(spans.length).toBeGreaterThan(0);
180+
181+
// When defaultColor=light, light is default, so we should have --custom-dark
182+
const span = spans[0];
183+
const style = span?.getAttribute('style');
184+
expect(style).toContain('--custom-dark');
185+
expect(style).not.toContain('--custom-light'); // light is the default, so no CSS variable
186+
expect(style).not.toContain('--shiki-');
187+
});
188+
});
189+
});

package/src/lib/component.tsx

Lines changed: 76 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -104,73 +104,80 @@ export const createShikiHighlighterComponent = (
104104
options?: HighlighterOptions
105105
) => React.ReactNode
106106
) => {
107-
return forwardRef<HTMLElement, ShikiHighlighterProps>(({
108-
language,
109-
theme,
110-
delay,
111-
transformers,
112-
defaultColor,
113-
cssVariablePrefix,
114-
addDefaultStyles = true,
115-
style,
116-
langStyle,
117-
className,
118-
langClassName,
119-
showLanguage = true,
120-
showLineNumbers = false,
121-
startingLineNumber = 1,
122-
children: code,
123-
as: Element = 'pre',
124-
customLanguages,
125-
...shikiOptions
126-
}, ref) => {
127-
// Destructure some options for use in hook
128-
const options: HighlighterOptions = {
129-
delay,
130-
transformers,
131-
customLanguages,
132-
showLineNumbers,
133-
startingLineNumber,
134-
...shikiOptions,
135-
};
136-
137-
// Use resolveLanguage to get displayLanguageId directly
138-
const { displayLanguageId } = resolveLanguage(
139-
language,
140-
customLanguages
141-
);
142-
143-
const highlightedCode = useShikiHighlighterImpl(
144-
code,
145-
language,
146-
theme,
147-
options
148-
);
149-
150-
return (
151-
<Element
152-
ref={ref}
153-
data-testid="shiki-container"
154-
className={clsx(
155-
'relative',
156-
'not-prose',
157-
addDefaultStyles && 'defaultStyles',
158-
className
159-
)}
160-
style={style}
161-
id="shiki-container"
162-
>
163-
{showLanguage && displayLanguageId ? (
164-
<span
165-
className={clsx('languageLabel', langClassName)}
166-
style={langStyle}
167-
id="language-label"
168-
>
169-
{displayLanguageId}
170-
</span>
171-
) : null}
172-
{highlightedCode}
173-
</Element>
174-
);
175-
});
107+
return forwardRef<HTMLElement, ShikiHighlighterProps>(
108+
(
109+
{
110+
language,
111+
theme,
112+
delay,
113+
transformers,
114+
defaultColor,
115+
cssVariablePrefix,
116+
addDefaultStyles = true,
117+
style,
118+
langStyle,
119+
className,
120+
langClassName,
121+
showLanguage = true,
122+
showLineNumbers = false,
123+
startingLineNumber = 1,
124+
children: code,
125+
as: Element = 'pre',
126+
customLanguages,
127+
...shikiOptions
128+
},
129+
ref
130+
) => {
131+
// Destructure some options for use in hook
132+
const options: HighlighterOptions = {
133+
delay,
134+
transformers,
135+
customLanguages,
136+
showLineNumbers,
137+
defaultColor,
138+
cssVariablePrefix,
139+
startingLineNumber,
140+
...shikiOptions,
141+
};
142+
143+
// Use resolveLanguage to get displayLanguageId directly
144+
const { displayLanguageId } = resolveLanguage(
145+
language,
146+
customLanguages
147+
);
148+
149+
const highlightedCode = useShikiHighlighterImpl(
150+
code,
151+
language,
152+
theme,
153+
options
154+
);
155+
156+
return (
157+
<Element
158+
ref={ref}
159+
data-testid="shiki-container"
160+
className={clsx(
161+
'relative',
162+
'not-prose',
163+
addDefaultStyles && 'defaultStyles',
164+
className
165+
)}
166+
style={style}
167+
id="shiki-container"
168+
>
169+
{showLanguage && displayLanguageId ? (
170+
<span
171+
className={clsx('languageLabel', langClassName)}
172+
style={langStyle}
173+
id="language-label"
174+
>
175+
{displayLanguageId}
176+
</span>
177+
) : null}
178+
{highlightedCode}
179+
</Element>
180+
);
181+
}
182+
);
176183
};

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)