Skip to content

Commit a8b3bf0

Browse files
committed
Add utility to set + read theme
1 parent aa89688 commit a8b3bf0

File tree

4 files changed

+150
-2
lines changed

4 files changed

+150
-2
lines changed

src/app.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4747

4848
export { PostMessageTransport } from "./message-transport";
4949
export * from "./types";
50-
export { applyHostStyles } from "./styles";
50+
export {
51+
applyHostStyles,
52+
getDocumentTheme,
53+
applyDocumentTheme,
54+
} from "./styles";
5155

5256
/**
5357
* Metadata key for associating a resource URI with a tool call.

src/react/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*
1111
* - {@link useApp} - React hook to create and connect an MCP App
1212
* - {@link useHostStyles} - React hook to apply host styles as CSS variables
13+
* - {@link useDocumentTheme} - React hook for reactive document theme
1314
* - {@link useAutoResize} - React hook for manual auto-resize control (rarely needed)
1415
*
1516
* @module @modelcontextprotocol/ext-apps/react
@@ -33,4 +34,5 @@
3334
*/
3435
export * from "./useApp";
3536
export * from "./useAutoResize";
37+
export * from "./useDocumentTheme";
3638
export * from "./useHostStyles";

src/react/useDocumentTheme.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useEffect, useState } from "react";
2+
import { getDocumentTheme } from "../styles";
3+
import { McpUiTheme } from "../types";
4+
5+
/**
6+
* React hook that provides the current document theme reactively.
7+
*
8+
* Uses a MutationObserver to watch for changes to the `data-theme` attribute
9+
* or `class` on `document.documentElement`. When the theme changes (e.g., from
10+
* host context updates), the hook automatically re-renders your component with
11+
* the new theme value.
12+
*
13+
* @returns The current theme ("light" or "dark")
14+
*
15+
* @example Conditionally render based on theme
16+
* ```tsx
17+
* import { useDocumentTheme } from '@modelcontextprotocol/ext-apps/react';
18+
*
19+
* function MyApp() {
20+
* const theme = useDocumentTheme();
21+
*
22+
* return (
23+
* <div>
24+
* {theme === 'dark' ? <DarkIcon /> : <LightIcon />}
25+
* </div>
26+
* );
27+
* }
28+
* ```
29+
*
30+
* @example Use with theme-aware styling
31+
* ```tsx
32+
* function ThemedButton() {
33+
* const theme = useDocumentTheme();
34+
*
35+
* return (
36+
* <button style={{
37+
* background: theme === 'dark' ? '#333' : '#fff',
38+
* color: theme === 'dark' ? '#fff' : '#333',
39+
* }}>
40+
* Click me
41+
* </button>
42+
* );
43+
* }
44+
* ```
45+
*
46+
* @see {@link getDocumentTheme} for the underlying function
47+
* @see {@link applyDocumentTheme} to set the theme
48+
*/
49+
export function useDocumentTheme(): McpUiTheme {
50+
const [theme, setTheme] = useState<McpUiTheme>(getDocumentTheme);
51+
52+
useEffect(() => {
53+
const observer = new MutationObserver(() => {
54+
setTheme(getDocumentTheme());
55+
});
56+
57+
observer.observe(document.documentElement, {
58+
attributes: true,
59+
attributeFilter: ["data-theme", "class"],
60+
characterData: false,
61+
childList: false,
62+
subtree: false,
63+
});
64+
65+
return () => observer.disconnect();
66+
}, []);
67+
68+
return theme;
69+
}

src/styles.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,77 @@
1-
import { McpUiStyles } from "./types";
1+
import { McpUiStyles, McpUiTheme } from "./types";
2+
3+
/**
4+
* Get the current document theme from the root HTML element.
5+
*
6+
* Reads the theme from the `data-theme` attribute on `document.documentElement`.
7+
* Falls back to checking for a `dark` class for compatibility with Tailwind CSS
8+
* dark mode conventions.
9+
*
10+
* @returns The current theme ("light" or "dark")
11+
*
12+
* @example Check current theme
13+
* ```typescript
14+
* import { getDocumentTheme } from '@modelcontextprotocol/ext-apps';
15+
*
16+
* const theme = getDocumentTheme();
17+
* console.log(`Current theme: ${theme}`);
18+
* ```
19+
*
20+
* @see {@link applyDocumentTheme} to set the theme
21+
* @see {@link McpUiTheme} for the theme type
22+
*/
23+
export function getDocumentTheme(): McpUiTheme {
24+
const theme = document.documentElement.getAttribute("data-theme");
25+
26+
if (theme === "dark" || theme === "light") {
27+
return theme;
28+
}
29+
30+
// Fallback: check for "dark" class (Tailwind CSS convention)
31+
const darkMode = document.documentElement.classList.contains("dark");
32+
33+
return darkMode ? "dark" : "light";
34+
}
35+
36+
/**
37+
* Apply a theme to the document root element.
38+
*
39+
* Sets the `data-theme` attribute and CSS `color-scheme` property on
40+
* `document.documentElement`. This enables CSS selectors like
41+
* `[data-theme="dark"]` and ensures native elements (scrollbars, form controls)
42+
* respect the theme.
43+
*
44+
* @param theme - The theme to apply ("light" or "dark")
45+
*
46+
* @example Apply theme from host context
47+
* ```typescript
48+
* import { applyDocumentTheme } from '@modelcontextprotocol/ext-apps';
49+
*
50+
* app.onhostcontextchanged = (params) => {
51+
* if (params.theme) {
52+
* applyDocumentTheme(params.theme);
53+
* }
54+
* };
55+
* ```
56+
*
57+
* @example Use with CSS selectors
58+
* ```css
59+
* [data-theme="dark"] {
60+
* --bg-color: #1a1a1a;
61+
* }
62+
* [data-theme="light"] {
63+
* --bg-color: #ffffff;
64+
* }
65+
* ```
66+
*
67+
* @see {@link getDocumentTheme} to read the current theme
68+
* @see {@link McpUiTheme} for the theme type
69+
*/
70+
export function applyDocumentTheme(theme: McpUiTheme): void {
71+
const root = document.documentElement;
72+
root.setAttribute("data-theme", theme);
73+
root.style.colorScheme = theme;
74+
}
275

376
/**
477
* Apply host styles as CSS custom properties on an element.

0 commit comments

Comments
 (0)