Skip to content

Commit f834892

Browse files
fix: codeblock dark mode and background (#40)
* Add theme provider * Update footer.tsx * Update code blocks * Create yellow-webs-follow.md * Accept two shiki themes * Update index.tsx * Misc fixes
1 parent 045907f commit f834892

File tree

12 files changed

+183
-71
lines changed

12 files changed

+183
-71
lines changed

.changeset/yellow-webs-follow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"streamdown": patch
3+
---
4+
5+
fix: codeblock dark mode and background
Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
1+
import { ModeToggle } from '@/components/mode-toggle';
2+
13
export const Footer = () => (
24
<footer className="px-4 py-8">
3-
<div className="text-center text-muted-foreground text-sm">
4-
<p>
5-
Made with 🖤 and 🤖 by{' '}
6-
<a
7-
className="font-medium text-blue-600 underline"
8-
href="https://vercel.com"
9-
rel="noopener"
10-
target="_blank"
11-
>
12-
Vercel
13-
</a>
14-
. View the{' '}
15-
<a
16-
className="font-medium text-blue-600 underline"
17-
href="https://github.com/vercel/streamdown"
18-
rel="noopener"
19-
target="_blank"
20-
>
21-
source code
22-
</a>
23-
.
24-
</p>
5+
<div className="flex items-center justify-between">
6+
<div className="text-muted-foreground text-sm">
7+
<p>
8+
Made with 🖤 and 🤖 by{' '}
9+
<a
10+
className="font-medium text-blue-600 underline"
11+
href="https://vercel.com"
12+
rel="noopener"
13+
target="_blank"
14+
>
15+
Vercel
16+
</a>
17+
. View the{' '}
18+
<a
19+
className="font-medium text-blue-600 underline"
20+
href="https://github.com/vercel/streamdown"
21+
rel="noopener"
22+
target="_blank"
23+
>
24+
source code
25+
</a>
26+
.
27+
</p>
28+
</div>
29+
<ModeToggle />
2530
</div>
2631
</footer>
2732
);

apps/website/app/components/header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ const Vercel = (props: SVGProps<SVGSVGElement>) => (
1414
);
1515

1616
export const Header = () => (
17-
<div className="sticky top-0 z-10 flex items-center justify-between bg-secondary p-4 backdrop-blur-sm">
17+
<div className="sticky top-0 z-10 flex items-center justify-between bg-secondary p-4 backdrop-blur-sm dark:bg-background">
1818
<div className="mx-auto flex items-center gap-1 sm:mx-0">
19-
<a href="https://vercel.com" className="flex items-center gap-1">
19+
<a className="flex items-center gap-1" href="https://vercel.com">
2020
<Vercel className="h-4 w-auto" />
2121
<span className="font-semibold sm:text-lg">Vercel</span>
2222
</a>

apps/website/app/components/props.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@ const props = [
5858
},
5959
{
6060
name: 'shikiTheme',
61-
type: 'BundledTheme (from Shiki)',
62-
default: 'github-light',
63-
description: 'The theme to use for code blocks.',
61+
type: '[BundledTheme, BundledTheme] (from Shiki)',
62+
default: '["github-light", "github-dark"]',
63+
description:
64+
'The themes to use for code blocks. Defaults to ["github-light", "github-dark"].',
6465
},
6566
];
6667

apps/website/app/layout.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Geist, Geist_Mono } from 'next/font/google';
22
import './globals.css';
33
import { Analytics } from '@vercel/analytics/next';
44
import type { ReactNode } from 'react';
5+
import { Providers } from '@/components/providers';
56
import { cn } from '@/lib/utils';
67

78
const geistSans = Geist({
@@ -23,16 +24,18 @@ type RootLayoutProps = {
2324
};
2425

2526
const RootLayout = ({ children }: RootLayoutProps) => (
26-
<html lang="en">
27+
<html lang="en" suppressHydrationWarning>
2728
<body
2829
className={cn(
2930
geistSans.variable,
3031
geistMono.variable,
3132
'overflow-x-hidden font-sans antialiased sm:px-4'
3233
)}
3334
>
34-
{children}
35-
<Analytics />
35+
<Providers>
36+
{children}
37+
<Analytics />
38+
</Providers>
3639
</body>
3740
</html>
3841
);

apps/website/components/code-block.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type HTMLAttributes,
88
useContext,
99
useEffect,
10+
useRef,
1011
useState,
1112
} from 'react';
1213
import { type BundledLanguage, codeToHtml } from 'shiki';
@@ -26,10 +27,16 @@ const CodeBlockContext = createContext<CodeBlockContextType>({
2627
});
2728

2829
export async function highlightCode(code: string, language: BundledLanguage) {
29-
return await codeToHtml(code, {
30-
lang: language,
31-
theme: 'github-light',
32-
});
30+
return Promise.all([
31+
await codeToHtml(code, {
32+
lang: language,
33+
theme: 'github-light',
34+
}),
35+
await codeToHtml(code, {
36+
lang: language,
37+
theme: 'github-dark',
38+
}),
39+
]);
3340
}
3441

3542
export const CodeBlock = ({
@@ -40,30 +47,44 @@ export const CodeBlock = ({
4047
...props
4148
}: CodeBlockProps) => {
4249
const [html, setHtml] = useState<string>('');
50+
const [darkHtml, setDarkHtml] = useState<string>('');
51+
const mounted = useRef(false);
4352

4453
useEffect(() => {
45-
let isMounted = true;
46-
47-
highlightCode(code, language).then((result) => {
48-
if (isMounted) {
49-
setHtml(result);
54+
highlightCode(code, language).then(([light, dark]) => {
55+
if (!mounted.current) {
56+
setHtml(light);
57+
setDarkHtml(dark);
58+
mounted.current = true;
5059
}
5160
});
5261

5362
return () => {
54-
isMounted = false;
63+
mounted.current = false;
5564
};
5665
}, [code, language]);
5766

5867
return (
5968
<CodeBlockContext.Provider value={{ code }}>
6069
<div className="group relative">
6170
<div
62-
className={cn('overflow-x-auto', className)}
71+
className={cn(
72+
'overflow-x-auto dark:hidden [&>pre]:bg-transparent!',
73+
className
74+
)}
6375
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
6476
dangerouslySetInnerHTML={{ __html: html }}
6577
{...props}
6678
/>
79+
<div
80+
className={cn(
81+
'hidden overflow-x-auto dark:block [&>pre]:bg-transparent!',
82+
className
83+
)}
84+
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
85+
dangerouslySetInnerHTML={{ __html: darkHtml }}
86+
{...props}
87+
/>
6788
{children}
6889
</div>
6990
</CodeBlockContext.Provider>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use client';
2+
3+
import { Moon, Sun } from 'lucide-react';
4+
import { useTheme } from 'next-themes';
5+
import { useEffect, useState } from 'react';
6+
import { Button } from '@/components/ui/button';
7+
8+
export function ModeToggle() {
9+
const [mounted, setMounted] = useState(false);
10+
const { theme, setTheme } = useTheme();
11+
12+
useEffect(() => {
13+
setMounted(true);
14+
}, []);
15+
16+
if (!mounted) {
17+
return null;
18+
}
19+
20+
return (
21+
<Button
22+
variant="ghost"
23+
size="icon"
24+
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
25+
aria-label="Toggle theme"
26+
>
27+
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
28+
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
29+
</Button>
30+
);
31+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use client';
2+
3+
import { ThemeProvider } from 'next-themes';
4+
import type { ReactNode } from 'react';
5+
6+
type ProvidersProps = {
7+
children: ReactNode;
8+
};
9+
10+
export function Providers({ children }: ProvidersProps) {
11+
return (
12+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
13+
{children}
14+
</ThemeProvider>
15+
);
16+
}

apps/website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"lucide-react": "^0.539.0",
1818
"motion": "^12.23.12",
1919
"next": "15.4.6",
20+
"next-themes": "^0.4.6",
2021
"react": "19.1.0",
2122
"react-dom": "19.1.0",
2223
"react-markdown": "^10.1.0",

packages/streamdown/index.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import hardenReactMarkdownImport from 'harden-react-markdown';
1111
import { components as defaultComponents } from './lib/components';
1212
import { parseMarkdownIntoBlocks } from './lib/parse-blocks';
1313
import { parseIncompleteMarkdown } from './lib/parse-incomplete-markdown';
14-
import { Mermaid } from './lib/mermaid';
1514
import { cn } from './lib/utils';
1615

1716
type HardenReactMarkdownProps = Options & {
@@ -22,6 +21,7 @@ type HardenReactMarkdownProps = Options & {
2221

2322
// Handle both ESM and CJS imports
2423
const hardenReactMarkdown =
24+
// biome-ignore lint/suspicious/noExplicitAny: "this is needed."
2525
(hardenReactMarkdownImport as any).default || hardenReactMarkdownImport;
2626

2727
// Create a hardened version of ReactMarkdown
@@ -31,10 +31,13 @@ const HardenedMarkdown: ReturnType<typeof hardenReactMarkdown> =
3131
export type StreamdownProps = HardenReactMarkdownProps & {
3232
parseIncompleteMarkdown?: boolean;
3333
className?: string;
34-
shikiTheme?: BundledTheme;
34+
shikiTheme?: [BundledTheme, BundledTheme];
3535
};
3636

37-
export const ShikiThemeContext = createContext<BundledTheme>('github-light');
37+
export const ShikiThemeContext = createContext<[BundledTheme, BundledTheme]>([
38+
'github-light' as BundledTheme,
39+
'github-dark' as BundledTheme,
40+
]);
3841

3942
type BlockProps = HardenReactMarkdownProps & {
4043
content: string;
@@ -69,7 +72,7 @@ export const Streamdown = memo(
6972
rehypePlugins,
7073
remarkPlugins,
7174
className,
72-
shikiTheme = 'github-light',
75+
shikiTheme = ['github-light', 'github-dark'],
7376
...props
7477
}: StreamdownProps) => {
7578
// Parse the children to remove incomplete markdown tokens if enabled
@@ -109,7 +112,3 @@ export const Streamdown = memo(
109112
prevProps.shikiTheme === nextProps.shikiTheme
110113
);
111114
Streamdown.displayName = 'Streamdown';
112-
113-
export { Mermaid };
114-
export { CodeBlock, CodeBlockCopyButton, CodeBlockRenderButton } from './lib/code-block';
115-
export default Streamdown;

0 commit comments

Comments
 (0)