AG-Grid React Series #48: Seamless Dark Mode Implementation in AG-Grid
Dark mode has become an indispensable feature in modern web applications, driven by user preference for reduced eye strain in low-light environments, improved battery life on OLED screens, and simply a more aesthetically pleasing interface for many. For data-intensive applications leveraging powerful components like AG-Grid, offering a dark mode is not just a trend but a significant enhancement to user experience and accessibility.
This installment of our AG-Grid React series will guide you through implementing a robust dark mode for your AG-Grid instances, covering built-in themes, user toggles, system preference detection, and custom styling to ensure your grids look stunning in any light.
Understanding AG-Grid's Theming System
AG-Grid provides a comprehensive theming system based on CSS classes. When you initialize an AG-Grid instance, you apply a theme class (e.g., ag-theme-alpine, ag-theme-balham) to the containing DOM element. This class then dictates the overall look and feel of the grid, including colors, fonts, borders, and more. Implementing dark mode primarily involves switching between a light and a dark theme class dynamically.
Method 1: Leveraging AG-Grid's Built-in Dark Themes
AG-Grid conveniently offers dark counterparts for its most popular themes. These themes are designed to complement their light versions, providing a consistent visual language while adapting to a dark aesthetic. The most commonly used dark themes include:
ag-theme-alpine-darkag-theme-balham-darkag-theme-quartz-dark
The simplest way to implement dark mode is to conditionally apply one of these dark themes based on your application's theme state. Let's see how this works in a React component:
import React, { useState, useMemo, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css'; // Core grid CSS
import 'ag-grid-community/styles/ag-theme-alpine.css'; // Light theme
import 'ag-grid-community/styles/ag-theme-alpine-dark.css'; // Dark theme
const MyGridComponent = () => {
const [isDarkMode, setIsDarkMode] = useState(false);
const rowData = useMemo(() => [
{ make: "Toyota", model: "Celica", price: 35000 },
{ make: "Ford", model: "Mondeo", price: 32000 },
{ make: "Porsche", model: "Boxster", price: 72000 }
], []);
const colDefs = useMemo(() => [
{ field: 'make' },
{ field: 'model' },
{ field: 'price' }
], []);
const handleToggleTheme = useCallback(() => {
setIsDarkMode(prevMode => !prevMode);
}, []);
const getThemeClass = () => {
return isDarkMode ? 'ag-theme-alpine-dark' : 'ag-theme-alpine';
};
return (
<div style={{ width: '100%', height: '500px' }}>
<button onClick={handleToggleTheme}>
Toggle to {isDarkMode ? 'Light Mode' : 'Dark Mode'}
</button>
<div className={getThemeClass()} style={{ height: 'calc(100% - 40px)', width: '100%' }}>
<AgGridReact
rowData={rowData}
columnDefs={colDefs}
animateRows={true}
/>
</div>
</div>
);
};
export default MyGridComponent;
In this example, we use a React useState hook to manage the isDarkMode state. The getThemeClass function then returns the appropriate AG-Grid theme class based on this state, which is applied to the parent div containing the AgGridReact component. A simple button allows the user to toggle between themes.
Method 2: Auto-Detecting User Preference (`prefers-color-scheme`)
A better user experience often involves respecting the user's system-wide dark mode preference. Modern browsers provide the prefers-color-scheme media feature, which you can query using JavaScript or CSS. This allows your application, and thus your AG-Grid, to automatically switch to dark mode if the user's operating system is set to dark mode.
import React, { useState, useEffect, useMemo } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import 'ag-grid-community/styles/ag-theme-alpine-dark.css';
const MyGridComponentWithPreference = () => {
// Initialize with system preference, default to light if not detected
const [isDarkMode, setIsDarkMode] = useState(
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (e) => {
setIsDarkMode(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, []); // Empty dependency array means this effect runs once on mount
const rowData = useMemo(() => [
{ make: "Tesla", model: "Model S", price: 90000 },
{ make: "Audi", model: "A8", price: 75000 },
{ make: "Volvo", model: "XC90", price: 60000 }
], []);
const colDefs = useMemo(() => [
{ field: 'make' },
{ field: 'model' },
{ field: 'price' }
], []);
const getThemeClass = () => {
return isDarkMode ? 'ag-theme-alpine-dark' : 'ag-theme-alpine';
};
return (
<div style={{ width: '100%', height: '500px' }}>
<p>Theme is currently: <strong>{isDarkMode ? 'Dark' : 'Light'}</strong> (based on system preference)</p>
<div className={getThemeClass()} style={{ height: 'calc(100% - 40px)', width: '100%' }}>
<AgGridReact
rowData={rowData}
columnDefs={colDefs}
animateRows={true}
/>
</div>
</div>
);
};
export default MyGridComponentWithPreference;
Here, the useState is initialized based on the current prefers-color-scheme. An useEffect hook then listens for changes to this preference and updates the state accordingly, ensuring the grid theme always matches the system setting.
Method 3: Combining User Toggle and System Preference with Persistence
The most robust solution often involves auto-detection combined with a user override, with the user's explicit choice being persisted (e.g., in localStorage). This provides the best of both worlds: a good default experience and full user control.
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import 'ag-grid-community/styles/ag-theme-alpine-dark.css';
const LOCAL_STORAGE_KEY = 'agGridThemePreference';
const MyGridComponentFullControl = () => {
// Initialize from localStorage, then system preference, default to light
const [themePreference, setThemePreference] = useState(() => {
const storedPref = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedPref !== null) {
return storedPref === 'dark';
}
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
});
// Effect to listen for system changes OR persist user choice
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemChange = (e) => {
// Only update if no explicit user preference is stored in current session
if (localStorage.getItem(LOCAL_STORAGE_KEY) === null) {
setThemePreference(e.matches);
}
};
mediaQuery.addEventListener('change', handleSystemChange);
// Clean up
return () => {
mediaQuery.removeEventListener('change', handleSystemChange);
};
}, []); // Run once on mount
// Effect to persist user's explicit theme preference
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, themePreference ? 'dark' : 'light');
}, [themePreference]);
const rowData = useMemo(() => [
{ make: "Tesla", model: "Model 3", price: 60000 },
{ make: "BMW", model: "M3", price: 85000 },
{ make: "Mercedes", model: "C-Class", price: 55000 }
], []);
const colDefs = useMemo(() => [
{ field: 'make' },
{ field: 'model' },
{ field: 'price' }
], []);
const handleToggleTheme = useCallback(() => {
setThemePreference(prevPref => !prevPref);
}, []);
const getThemeClass = () => {
return themePreference ? 'ag-theme-alpine-dark' : 'ag-theme-alpine';
};
return (
<div style={{ width: '100%', height: '500px' }}>
<button onClick={handleToggleTheme}>
Switch to {themePreference ? 'Light Mode' : 'Dark Mode'}
</button>
<div className={getThemeClass()} style={{ height: 'calc(100% - 40px)', width: '100%' }}>
<AgGridReact
rowData={rowData}
columnDefs={colDefs}
animateRows={true}
/>
</div>
</div>
);
};
export default MyGridComponentFullControl;
This approach combines localStorage for persistence, initial state from localStorage (or system preference), and an event listener for system theme changes. Crucially, the system preference listener only updates the theme if the user hasn't explicitly set a preference in the current session (i.e., localStorage is empty for this key).
Customizing Dark Mode Styles for AG-Grid
While AG-Grid's built-in dark themes are excellent, you might need to align them more closely with your application's specific branding or color palette. AG-Grid is highly customizable via CSS. You can either override specific CSS rules or, more effectively, use CSS variables.
Using CSS Variables
AG-Grid themes are built using CSS variables (also known as custom properties). This makes customization very straightforward. You can define your own values for these variables within your theme class scope.
/* In your global or component-specific CSS file */
/* Define variables for your light theme */
.ag-theme-my-custom-light {
--ag-foreground-color: #333;
--ag-background-color: #f7f7f7;
--ag-header-background-color: #eaeaea;
--ag-odd-row-background-color: #fcfcfc;
--ag-border-color: #ddd;
/* ... more AG-Grid variables for a light theme ... */
}
/* Define variables for your dark theme */
.ag-theme-my-custom-dark {
--ag-foreground-color: #e0e0e0;
--ag-background-color: #2b2b2b;
--ag-header-background-color: #3e3e3e;
--ag-odd-row-background-color: #353535;
--ag-border-color: #4a4a4a;
/* ... more AG-Grid variables for a dark theme ... */
}
Then, in your React component, you'd apply ag-theme-my-custom-light or ag-theme-my-custom-dark.
Overriding Specific CSS Rules
For more granular control or to tweak elements not directly covered by a CSS variable, you can target AG-Grid's CSS classes directly. Ensure your custom CSS is loaded after AG-Grid's default themes to ensure it takes precedence.
/* In your custom CSS file */
.ag-theme-alpine-dark {
/* Example: Change header text color */
.ag-header-cell-text {
color: #FFD700; /* Gold color for header text */
}
/* Example: Change hovered row background */
.ag-row-hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Example: Change scrollbar thumb color (this might be more complex and browser-specific) */
/* For Webkit browsers (Chrome, Safari) */
&::-webkit-scrollbar-thumb {
background-color: #555;
}
}
When overriding, it's often best to scope your overrides within the theme class (e.g., .ag-theme-alpine-dark .ag-header-cell-text) to ensure they only apply when that specific theme is active.
Integrating with a Global Dark Mode Strategy
In larger React applications, you'll likely have a global dark mode context or state management solution (e.g., React Context API, Redux, Zustand) that controls the theme across your entire application. AG-Grid should seamlessly integrate into this. Instead of useState within the grid component, you'd consume the global theme state and apply the appropriate AG-Grid class.
// Example using a simple React Context
// ThemeContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [isGlobalDarkMode, setIsGlobalDarkMode] = useState(() => {
const storedPref = localStorage.getItem('globalAppTheme');
if (storedPref !== null) {
return storedPref === 'dark';
}
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
});
useEffect(() => {
localStorage.setItem('globalAppTheme', isGlobalDarkMode ? 'dark' : 'light');
}, [isGlobalDarkMode]);
const toggleGlobalTheme = () => {
setIsGlobalDarkMode(prev => !prev);
};
return (
<ThemeContext.Provider value={{ isGlobalDarkMode, toggleGlobalTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
// MyGridComponentIntegrated.jsx
import React, { useMemo } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import 'ag-grid-community/styles/ag-theme-alpine-dark.css';
import { useTheme } from './ThemeContext'; // Your global theme context
const MyGridComponentIntegrated = () => {
const { isGlobalDarkMode, toggleGlobalTheme } = useTheme();
const rowData = useMemo(() => [
{ make: "Hyundai", model: "Tucson", price: 28000 },
{ make: "Kia", model: "Sportage", price: 29000 },
{ make: "Mazda", model: "CX-5", price: 30000 }
], []);
const colDefs = useMemo(() => [
{ field: 'make' },
{ field: 'model' },
{ field: 'price' }
], []);
const getThemeClass = () => {
// Use the global theme state
return isGlobalDarkMode ? 'ag-theme-alpine-dark' : 'ag-theme-alpine';
};
return (
<div style={{ width: '100%', height: '500px' }}>
<button onClick={toggleGlobalTheme}>
Toggle Global Theme (currently {isGlobalDarkMode ? 'Dark' : 'Light'})
</button>
<div className={getThemeClass()} style={{ height: 'calc(100% - 40px)', width: '100%' }}>
<AgGridReact
rowData={rowData}
columnDefs={colDefs}
animateRows={true}
/>
</div>
</div>
);
};
export default MyGridComponentIntegrated;
// In your App.js or main entry point:
// import { ThemeProvider } from './ThemeContext';
// import MyGridComponentIntegrated from './MyGridComponentIntegrated';
// function App() {
// return (
// <ThemeProvider>
// <MyGridComponentIntegrated />
// </ThemeProvider>
// );
// }
// export default App;
By consuming the global theme context, your AG-Grid components will automatically synchronize their appearance with the rest of your application, providing a cohesive user experience.
Best Practices for Dark Mode
- Consistency: Ensure all elements of your application, including third-party components like AG-Grid, adhere to the chosen dark mode palette. Inconsistent theming can be jarring.
- Contrast Ratio: Pay close attention to contrast ratios to maintain readability and accessibility. Tools like WebAIM Contrast Checker can help ensure your colors meet WCAG standards.
- Testing: Test your dark mode implementation across different browsers and devices to catch any unexpected rendering issues.
- User Preference Persistence: Always save the user's explicit theme choice (e.g., in
localStorage) so that their preference is remembered across sessions. - Smooth Transitions: If possible, use CSS transitions for color changes to make the theme switch less abrupt and more visually pleasing.
Conclusion
Implementing dark mode in AG-Grid is a straightforward yet impactful way to enhance the user experience of your React applications. By leveraging AG-Grid's built-in dark themes, respecting system preferences, allowing user toggles, and applying custom styles when needed, you can ensure your data grids are both functional and visually appealing in any environment. This flexibility is a testament to AG-Grid's robust styling capabilities, empowering developers to create modern and user-centric interfaces.