Skip to main content

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

  1. Overview
  2. Light/Dark Mode
  3. Using the Theme System
  4. Custom Color Schemes
  5. CSS Variables Reference
  6. 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:

  1. LocalStorage: Checks for saved user preference (framework-m-theme)
  2. System Preference: Uses prefers-color-scheme media query
  3. Default: Falls back to light theme

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.