Theming in Framework M
This guide covers the theming system in Framework M's frontend, including light/dark mode, custom color schemes, and user preference management.
Table of Contents
- Overview
- Light/Dark Mode
- Using the Theme System
- Custom Color Schemes
- CSS Variables Reference
- Best Practices
Overview
Framework M includes a built-in theming system that provides:
- Light/Dark Mode: Automatic theme switching with user preference persistence
- System Theme Detection: Respects OS-level dark mode preferences
- LocalStorage Persistence: User preferences saved across sessions
- CSS Variables: Easy customization of all colors and styles
- React Context: Theme state accessible throughout the app
Features
✅ Automatic Detection: Detects system theme preference on first load
✅ Toggle Component: Icon-based theme switcher in navbar
✅ Smooth Transitions: CSS transitions for theme changes
✅ Persistent: Saves preference in localStorage
✅ Accessible: Keyboard navigation and ARIA labels
Light/Dark Mode
How It Works
The theme system uses a React Context (ThemeContext) to manage the current theme state and CSS variables for styling.
Theme Resolution Order:
- LocalStorage: Checks for saved user preference (
framework-m-theme) - System Preference: Uses
prefers-color-schememedia query - Default: Falls back to
lighttheme
CSS Implementation:
The dark class is applied to the <html> element when dark mode is active:
<!-- Light mode -->
<html>
<!-- Dark mode -->
<html class="dark"></html>
</html>
All color variables are defined in :root for light mode and .dark for dark mode in App.css.
Using the Theme System
Basic Usage
Import and use the useTheme hook in any component:
import { useTheme } from '../contexts/ThemeContext';
function MyComponent() {
const { theme, setTheme, toggleTheme } = useTheme();
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
<button onClick={() => setTheme('dark')}>Force Dark</button>
<button onClick={() => setTheme('light')}>Force Light</button>
</div>
);
}
ThemeContext API
interface ThemeContextType {
theme: Theme; // Current theme: "light" | "dark"
setTheme: (theme: Theme) => void; // Set theme explicitly
toggleTheme: () => void; // Toggle between light and dark
}
ThemeProvider Setup
The ThemeProvider is already configured in App.tsx:
// src/App.tsx
import { ThemeProvider } from './contexts/ThemeContext';
export const App = () => (
<BrowserRouter>
<ThemeProvider>
<AppContent />
</ThemeProvider>
</BrowserRouter>
);
ThemeToggle Component
The navbar includes a ThemeToggle button:
// src/layout/Navbar.tsx
import { ThemeToggle } from '../components/ThemeToggle';
export function Navbar() {
return (
<header>
{/* Other navbar items */}
<ThemeToggle />
</header>
);
}
Features:
- Sun icon in dark mode (click to switch to light)
- Moon icon in light mode (click to switch to dark)
- Tooltip showing current theme
- Keyboard accessible
- Smooth hover/active states
Custom Color Schemes
Using CSS Variables
All colors are defined as CSS variables. To customize, override them:
/* Custom color scheme */
:root {
--color-primary: #8b5cf6; /* Purple primary */
--color-primary-dark: #7c3aed;
--color-success: #10b981; /* Emerald green */
}
.dark {
--color-primary: #a78bfa; /* Lighter purple for dark mode */
--color-primary-dark: #8b5cf6;
}
Per-Component Styling
Use CSS variables in your components:
function CustomButton() {
return (
<button
style={{
background: 'var(--color-primary)',
color: 'white',
border: 'none',
padding: '0.5rem 1rem',
borderRadius: 'var(--radius-md)',
}}
>
Click Me
</button>
);
}
Theme-Aware Components
Create components that respond to theme changes:
function ThemedCard({ children }: { children: React.ReactNode }) {
const { theme } = useTheme();
return (
<div
style={{
background: 'var(--color-bg)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-lg)',
padding: 'var(--spacing-lg)',
boxShadow: theme === 'dark' ? 'var(--shadow-md)' : 'var(--shadow-sm)',
}}
>
{children}
</div>
);
}
CSS Variables Reference
Light Mode Colors (:root)
--color-bg: #ffffff; /* Main background */
--color-bg-secondary: #f8fafc; /* Secondary background */
--color-bg-tertiary: #f1f5f9; /* Tertiary background */
--color-border: #e2e8f0; /* Border color */
--color-text: #0f172a; /* Primary text */
--color-text-secondary: #64748b; /* Secondary text */
--color-text-muted: #94a3b8; /* Muted text */
--color-primary: #0ea5e9; /* Primary brand color */
--color-primary-dark: #0284c7; /* Primary hover/active */
--color-sidebar: #1e293b; /* Sidebar background */
--color-sidebar-text: #e2e8f0; /* Sidebar text */
--color-sidebar-hover: #334155; /* Sidebar hover state */
--color-success: #22c55e; /* Success state */
--color-warning: #f59e0b; /* Warning state */
--color-error: #ef4444; /* Error state */
Dark Mode Colors (.dark)
--color-bg: #0f172a; /* Main background (dark) */
--color-bg-secondary: #1e293b; /* Secondary background (dark) */
--color-bg-tertiary: #334155; /* Tertiary background (dark) */
--color-border: #334155; /* Border color (dark) */
--color-text: #f1f5f9; /* Primary text (light) */
--color-text-secondary: #cbd5e1; /* Secondary text (light) */
--color-text-muted: #94a3b8; /* Muted text */
--color-primary: #38bdf8; /* Primary brand (lighter) */
--color-primary-dark: #0ea5e9; /* Primary hover/active */
--color-sidebar: #020617; /* Sidebar background (darker) */
--color-sidebar-text: #e2e8f0; /* Sidebar text */
--color-sidebar-hover: #1e293b; /* Sidebar hover state */
Spacing
--spacing-xs: 0.25rem; /* 4px */
--spacing-sm: 0.5rem; /* 8px */
--spacing-md: 1rem; /* 16px */
--spacing-lg: 1.5rem; /* 24px */
--spacing-xl: 2rem; /* 32px */
Border Radius
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
Shadows
/* Light mode */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
/* Dark mode */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
Typography
--font-sans:
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
Best Practices
1. Always Use CSS Variables
// ✅ Good: Uses CSS variable
<div style={{ background: 'var(--color-bg)' }} />
// ❌ Bad: Hardcoded color
<div style={{ background: '#ffffff' }} />
2. Test in Both Themes
Always test your components in both light and dark mode:
// Use the theme toggle in navbar to switch modes
// Ensure text is readable and colors have good contrast
3. Use Semantic Color Names
/* ✅ Good: Semantic names */
--color-success: #22c55e;
--color-error: #ef4444;
/* ❌ Bad: Color-based names */
--color-green: #22c55e;
--color-red: #ef4444;
4. Respect User Preference
// ✅ Good: Let users choose
const { theme, toggleTheme } = useTheme();
// ❌ Bad: Force a theme
document.documentElement.classList.add("dark"); // Don't do this
5. Provide Visual Feedback
.theme-toggle {
transition: all 0.2s ease;
}
.theme-toggle:hover {
background: var(--hover-bg);
}
.theme-toggle:active {
transform: scale(0.95);
}
6. Consider Accessibility
// Include ARIA labels
<button
onClick={toggleTheme}
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{/* Icon */}
</button>
Advanced Usage
Custom Theme Provider
Create a custom theme provider for additional themes:
// src/contexts/CustomThemeContext.tsx
type ExtendedTheme = 'light' | 'dark' | 'blue' | 'purple';
export function CustomThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<ExtendedTheme>('light');
useEffect(() => {
// Apply custom theme classes
document.documentElement.className = theme;
}, [theme]);
return (
<CustomThemeContext.Provider value={{ theme, setTheme }}>
{children}
</CustomThemeContext.Provider>
);
}
/* Define custom themes */
.blue {
--color-primary: #3b82f6;
--color-bg: #eff6ff;
}
.purple {
--color-primary: #8b5cf6;
--color-bg: #faf5ff;
}
Syncing with Backend
Store user theme preference in backend:
import { useUpdate } from "@refinedev/core";
function ThemeSyncer() {
const { theme } = useTheme();
const { mutate } = useUpdate();
useEffect(() => {
// Save to UserPreferences
mutate({
resource: "UserPreferences",
id: "current",
values: { theme },
});
}, [theme, mutate]);
return null;
}
Per-Tenant Theming
Combine with tenant context for custom branding:
import { useTenant } from '../contexts/TenantContext';
import { useTheme } from '../contexts/ThemeContext';
function BrandedApp() {
const { attributes } = useTenant();
const { theme } = useTheme();
useEffect(() => {
// Apply tenant brand colors
if (attributes.primary_color) {
document.documentElement.style.setProperty(
'--color-primary',
attributes.primary_color
);
}
}, [attributes]);
return <div>{/* App content */}</div>;
}
Troubleshooting
Theme Not Persisting
Problem: Theme resets to light on page reload
Solution: Check localStorage is enabled:
// Test localStorage
try {
localStorage.setItem("test", "test");
localStorage.removeItem("test");
console.log("localStorage is working");
} catch (e) {
console.error("localStorage is disabled");
}
Flicker on Page Load
Problem: Brief flash of light theme before dark mode applies
Solution: Add inline script in index.html before React loads:
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<script>
// Apply theme immediately (before React loads)
const savedTheme = localStorage.getItem("framework-m-theme");
const systemDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
const theme = savedTheme || (systemDark ? "dark" : "light");
if (theme === "dark") {
document.documentElement.classList.add("dark");
}
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
Colors Not Updating
Problem: CSS variables not reflecting in components
Solution: Ensure you're using var() syntax:
/* ✅ Correct */
background: var(--color-bg);
/* ❌ Wrong */
background: --color-bg;
Summary
Framework M's theming system provides:
- ✅ Light/Dark Mode: Built-in toggle with user preference
- ✅ System Detection: Respects OS theme preference
- ✅ Persistent: Saves to localStorage
- ✅ Customizable: CSS variables for all colors
- ✅ Accessible: Keyboard navigation and ARIA support
- ✅ Extensible: Easy to add custom themes
The system is production-ready and can be extended for tenant-specific branding, custom color schemes, or additional theme variants.