How Dark Mode Works
Platforms like OpenAI and Claude provide theme information to widgets. FrontMCP UI can detect this and apply appropriate styles.
// The MCP Bridge exposes theme info
window.mcpBridge.theme // 'light' | 'dark'
// Or via context
window.mcpBridge.context.theme // 'light' | 'dark' | 'system'
Creating a Dark Theme
Create a separate dark theme:
import { createTheme } from '@frontmcp/ui';
const darkTheme = createTheme({
name: 'dark',
colors: {
semantic: {
primary: '#60a5fa', // Lighter blue for dark mode
secondary: '#9ca3af',
accent: '#a78bfa',
success: '#34d399',
warning: '#fbbf24',
danger: '#f87171',
info: '#38bdf8',
},
surface: {
background: '#0f172a', // Slate-900
foreground: '#1e293b', // Slate-800
muted: '#334155', // Slate-700
subtle: '#475569', // Slate-600
},
text: {
primary: '#f1f5f9', // Slate-100
secondary: '#cbd5e1', // Slate-300
muted: '#94a3b8', // Slate-400
inverse: '#0f172a',
link: '#60a5fa',
},
border: {
default: '#334155', // Slate-700
strong: '#475569', // Slate-600
muted: '#1e293b', // Slate-800
},
},
});
Detect the platform theme and apply the appropriate theme:
import { baseLayout, DEFAULT_THEME, createTheme } from '@frontmcp/ui';
const lightTheme = DEFAULT_THEME;
const darkTheme = createTheme({ name: 'dark', /* ... */ });
@Tool({
name: 'my_tool',
ui: {
template: (ctx) => {
// Platform theme is available via context
const isDark = typeof window !== 'undefined' &&
window.__mcpHostContext?.theme === 'dark';
return baseLayout({
content: myContent,
theme: isDark ? darkTheme : lightTheme,
});
},
},
})
Use prefers-color-scheme for automatic detection:
const html = `
<style>
:root {
--bg: #ffffff;
--text: #1f2937;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1f2937;
--text: #f9fafb;
}
}
</style>
<div style="background: var(--bg); color: var(--text);">
Content adapts to system preference
</div>
`;
Tailwind Dark Mode
Tailwind CSS supports dark mode variants:
const html = `
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<h1 class="text-2xl font-bold">Adaptive Content</h1>
<p class="text-gray-600 dark:text-gray-400">
This text adapts to dark mode.
</p>
</div>
`;
Tailwind’s dark mode requires the dark class on a parent element (usually <html>).
The base layout handles this automatically based on theme detection.
Manual Toggle
Allow users to toggle dark mode manually:
import { button } from '@frontmcp/ui';
const toggleButton = button('Toggle Dark Mode', {
data: { 'toggle-theme': 'true' },
});
const html = `
<script>
document.querySelector('[data-toggle-theme]').addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
});
</script>
${toggleButton}
`;
Theme-Aware Components
Components automatically use CSS variables, so they work with any theme:
import { card, badge, button } from '@frontmcp/ui';
// These components use CSS variables like:
// - bg-primary -> var(--color-primary)
// - text-text-primary -> var(--color-text-primary)
// - border-border -> var(--color-border)
// They automatically adapt when CSS variables change
const html = card(`
${badge('Status', { variant: 'success' })}
<p class="text-text-secondary">Description</p>
${button('Action')}
`, { title: 'Card Title' });
Dual Theme Setup
Create a complete light/dark setup:
import { createTheme, DEFAULT_THEME } from '@frontmcp/ui';
// Light theme (default)
export const lightTheme = DEFAULT_THEME;
// Dark theme
export const darkTheme = createTheme({
name: 'dark',
colors: {
semantic: {
primary: '#60a5fa',
secondary: '#9ca3af',
accent: '#a78bfa',
success: '#34d399',
warning: '#fbbf24',
danger: '#f87171',
info: '#38bdf8',
},
surface: {
background: '#0f172a',
foreground: '#1e293b',
muted: '#334155',
subtle: '#475569',
},
text: {
primary: '#f1f5f9',
secondary: '#cbd5e1',
muted: '#94a3b8',
inverse: '#0f172a',
link: '#60a5fa',
},
border: {
default: '#334155',
strong: '#475569',
muted: '#1e293b',
},
},
});
// Get theme based on mode
export function getTheme(mode: 'light' | 'dark' | 'system') {
if (mode === 'dark') return darkTheme;
if (mode === 'light') return lightTheme;
// System preference
if (typeof window !== 'undefined') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return prefersDark ? darkTheme : lightTheme;
}
return lightTheme;
}
Usage:
import { getTheme } from './themes';
import { baseLayout } from '@frontmcp/ui';
@Tool({
name: 'my_tool',
ui: {
template: (ctx) => {
const themeMode = window.__mcpHostContext?.theme ?? 'system';
const theme = getTheme(themeMode);
return baseLayout({
content: myContent,
theme,
});
},
},
})
Testing Dark Mode
Test both themes in development:
// Force dark mode for testing
document.documentElement.classList.add('dark');
// Or toggle
document.documentElement.classList.toggle('dark');
// Check current mode
const isDark = document.documentElement.classList.contains('dark');
OpenAI
OpenAI provides theme via window.openai.theme:
const isDark = window.openai?.theme === 'dark';
Claude
Claude Artifacts may have limited theme support. Default to light theme with system preference fallback.
Gemini
Check window.__mcpHostContext for theme information.
Best Practices
- Test both modes - Ensure all content is readable
- Check contrast ratios - Meet WCAG accessibility standards
- Use semantic colors - Let the theme define actual colors
- Respect user preference - Follow system/platform settings
- Provide graceful fallback - Default to light theme if detection fails
- Avoid hardcoded colors - Use CSS variables instead