Skip to main content

Synchronizing light and dark modes when using Material UI with Docusaurus

Daniel Farlow
Software Engineer

This post details how to synchronize Material UI's light and dark palettes with Docusaurus's light and dark modes, respectively.

TLDR

To get MUI to play nicely with Docusaurus when it comes to light and dark modes, paste the following code after your normal import statements for a React component within which you plan to utilize MUI functionality:

import { useColorMode } from '@docusaurus/theme-common';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const muiLightTheme = createTheme({ palette: { mode: 'light' } });
const muiDarkTheme = createTheme({ palette: { mode: 'dark' } });

Paste the following between the component function signature and its return statement:

const { colorMode } = useColorMode();
const muiTheme = colorMode === 'dark' ? muiDarkTheme : muiLightTheme;

Finally, wrap everything in the return statement within ThemeProvider in the following manner:

...
return (
<ThemeProvider theme={muiTheme}>
...
</ThemeProvider>
);

Problem description and context

Getting Material UI (MUI) to work with a site powered by Docusaurus may not seem to be all that difficult at first — you can plug and play and largely be happy to see MUI components behaving as expected. Unless you have light and dark modes enabled for your Docusaurus site (i.e., most Docusaurus sites). This can become a real problem.

For example, suppose we are interested in using MUI's DataGrid component. If we copy the code for the very first example in the MUI docs for the DataGrid component, then we will get the default behavior as shown below in the first tab (toggle the light and dark modes for this site to see the problem). The default behavior, for this site at least, is that the data grid is perfectly fine in light mode, but it quickly becomes almost incomprehensible in dark mode. We need a solution that will produce the behavior illustrated below in the second tab (toggle the light and dark modes for this site again to note the difference).

One of many possible solutions

There are many possible solutions to the problem outlined above. I will present a stunningly simple solution that has worked for me — if you feel you have a better solution, then please share!

The following code was lifted directly from MUI's docs for the DataGrid example above (default behavior):

import * as React from 'react';
import Box from '@mui/material/Box';
import { DataGrid } from '@mui/x-data-grid';

const columns = [
{ field: 'id', headerName: 'ID', width: 90 },
{
field: 'firstName',
headerName: 'First name',
width: 150,
editable: true,
},
{
field: 'lastName',
headerName: 'Last name',
width: 150,
editable: true,
},
{
field: 'age',
headerName: 'Age',
type: 'number',
width: 110,
editable: true,
},
{
field: 'fullName',
headerName: 'Full name',
description: 'This column has a value getter and is not sortable.',
sortable: false,
width: 160,
valueGetter: (params) =>
`${params.row.firstName || ''} ${params.row.lastName || ''}`,
},
];

const rows = [
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
{ id: 5, lastName: 'Targaryen', firstName: 'Daenerys', age: null },
{ id: 6, lastName: 'Melisandre', firstName: null, age: 150 },
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
];

export default function SampleDataGrid() {
return (
<Box sx={{ height: 400, width: '100%' }}>
<DataGrid
rows={rows}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
checkboxSelection
disableSelectionOnClick
experimentalFeatures={{ newEditingApi: true }}
/>
</Box>
);
}

To get the desired behavior, we just need to make the following code changes (only highlighted lines have been changed):

import * as React from 'react';
import Box from '@mui/material/Box';
import { DataGrid } from '@mui/x-data-grid';
import { useColorMode } from '@docusaurus/theme-common';
import { ThemeProvider, createTheme } from '@mui/material/styles';
const muiLightTheme = createTheme({ palette: { mode: 'light' } });
const muiDarkTheme = createTheme({ palette: { mode: 'dark' } });

const columns = [
{ field: 'id', headerName: 'ID', width: 90 },
{
field: 'firstName',
headerName: 'First name',
width: 150,
editable: true,
},
{
field: 'lastName',
headerName: 'Last name',
width: 150,
editable: true,
},
{
field: 'age',
headerName: 'Age',
type: 'number',
width: 110,
editable: true,
},
{
field: 'fullName',
headerName: 'Full name',
description: 'This column has a value getter and is not sortable.',
sortable: false,
width: 160,
valueGetter: (params) =>
`${params.row.firstName || ''} ${params.row.lastName || ''}`,
},
];

const rows = [
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
{ id: 5, lastName: 'Targaryen', firstName: 'Daenerys', age: null },
{ id: 6, lastName: 'Melisandre', firstName: null, age: 150 },
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
];

export default function SampleDataGrid() {
const { colorMode } = useColorMode();
const muiTheme = colorMode === 'dark' ? muiDarkTheme : muiLightTheme;
return (
<ThemeProvider theme={muiTheme}>
<Box sx={{ height: 400, width: '100%' }}>
<DataGrid
rows={rows}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
checkboxSelection
disableSelectionOnClick
experimentalFeatures={{ newEditingApi: true }}
/>
</Box>
</ThemeProvider>
);
}

Refinements

An immediate objection you may have to the solution above is that the styling for all of your components that use MUI will be different from the styling of your site. This is true. As with nearly all development tasks, you have a tradeoff decision to make: simplicity at the cost of quality or quality at the cost of simplicity?

In the solution above, the default MUI light and dark themes are being used, respectively:

const muiLightTheme = createTheme({ palette: { mode: 'light' } });
const muiDarkTheme = createTheme({ palette: { mode: 'dark' } });

How are these light and dark themes actually defined though? As the MUI docs note:

The palette enables you to modify the color of the components to suit your brand.

The palette's default values may be found using MUI's theme explorer or by opening the dev tools console on the default values page and exploring window.theme.palette. As noted on MUI's dark mode page:

You can make your application use the dark theme as the default—regardless of the user's preference—by adding mode: 'dark' to the createTheme helper:

import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';

const darkTheme = createTheme({
palette: {
mode: 'dark',
},
});

function App() {
return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<main>This app is using the dark mode</main>
</ThemeProvider>
);
}

export default App;

Adding mode: 'dark' to the createTheme helper modifies several palette values [...].

Adding <CssBaseline /> inside of the <ThemeProvider> component will also enable dark mode for the app's background.

Note: setting the dark mode this way only works if you are using the default palette. If you have a custom palette, make sure that you have the correct values based on the mode. [...]

The takeaway from the excerpts above? We are only interested in using the default palettes for light and dark mode. Only the highlighted lines of code above are of any interest to us for the very simple solution. If you want to get fancy with your palettes, ostensibly to make MUI's light and dark modes match those of your Docusaurus site, then you may be in for a bit of work. MUI's default light and dark palettes are included below for reference in case you want to really customize things.

Light and dark palettes for Material UI (default values)

Visit MUI's default theme page and run the following in the console (assuming you are using Chrome):

copy(JSON.stringify(window.theme.palette,null,2))

Run the code above twice (once for when you have chosen light mode on the default theme page and again for when you have chosen dark mode). The resultant values are shown in the first two tabs below. You can also run

copy(JSON.stringify(window.theme,null,2))

to get the complete themes (shown in the last two tabs below).

{
"mode": "light",
"primary": {
"50": "#F0F7FF",
"100": "#C2E0FF",
"200": "#99CCF3",
"300": "#66B2FF",
"400": "#3399FF",
"500": "#007FFF",
"600": "#0072E5",
"700": "#0059B2",
"800": "#004C99",
"900": "#003A75",
"main": "#007FFF",
"light": "#66B2FF",
"dark": "#0059B2",
"contrastText": "#fff"
},
"divider": "#E7EBF0",
"primaryDark": {
"50": "#E2EDF8",
"100": "#CEE0F3",
"200": "#91B9E3",
"300": "#5090D3",
"400": "#265D97",
"500": "#1E4976",
"600": "#173A5E",
"700": "#132F4C",
"800": "#001E3C",
"900": "#0A1929",
"main": "#5090D3"
},
"common": {
"black": "#1D1D1D",
"white": "#fff"
},
"text": {
"primary": "#1A2027",
"secondary": "#3E5060",
"disabled": "rgba(0, 0, 0, 0.38)",
"primaryChannel": "0 0 0",
"secondaryChannel": "0 0 0"
},
"grey": {
"50": "#F3F6F9",
"100": "#E7EBF0",
"200": "#E0E3E7",
"300": "#CDD2D7",
"400": "#B2BAC2",
"500": "#A0AAB4",
"600": "#6F7E8C",
"700": "#3E5060",
"800": "#2D3843",
"900": "#1A2027",
"main": "#E7EBF0",
"contrastText": "#6F7E8C",
"A100": "#f5f5f5",
"A200": "#eeeeee",
"A400": "#bdbdbd",
"A700": "#616161"
},
"error": {
"50": "#FFF0F1",
"100": "#FFDBDE",
"200": "#FFBDC2",
"300": "#FF99A2",
"400": "#FF7A86",
"500": "#FF505F",
"600": "#EB0014",
"700": "#C70011",
"800": "#94000D",
"900": "#570007",
"main": "#EB0014",
"light": "#FF99A2",
"dark": "#C70011",
"contrastText": "#fff"
},
"success": {
"50": "#E9FBF0",
"100": "#C6F6D9",
"200": "#9AEFBC",
"300": "#6AE79C",
"400": "#3EE07F",
"500": "#21CC66",
"600": "#1DB45A",
"700": "#1AA251",
"800": "#178D46",
"900": "#0F5C2E",
"main": "#1AA251",
"light": "#6AE79C",
"dark": "#1AA251",
"contrastText": "#fff"
},
"warning": {
"50": "#FFF9EB",
"100": "#FFF3C1",
"200": "#FFECA1",
"300": "#FFDC48",
"400": "#F4C000",
"500": "#DEA500",
"600": "#D18E00",
"700": "#AB6800",
"800": "#8C5800",
"900": "#5A3600",
"main": "#DEA500",
"light": "#FFDC48",
"dark": "#AB6800",
"contrastText": "rgba(0, 0, 0, 0.87)"
},
"secondary": {
"main": "#9c27b0",
"light": "#ba68c8",
"dark": "#7b1fa2",
"contrastText": "#fff"
},
"info": {
"main": "#0288d1",
"light": "#03a9f4",
"dark": "#01579b",
"contrastText": "#fff"
},
"contrastThreshold": 3,
"tonalOffset": 0.2,
"background": {
"paper": "#fff",
"default": "#fff"
},
"action": {
"active": "rgba(0, 0, 0, 0.54)",
"hover": "rgba(0, 0, 0, 0.04)",
"hoverOpacity": 0.04,
"selected": "rgba(0, 0, 0, 0.08)",
"selectedOpacity": 0.08,
"disabled": "rgba(0, 0, 0, 0.26)",
"disabledBackground": "rgba(0, 0, 0, 0.12)",
"disabledOpacity": 0.38,
"focus": "rgba(0, 0, 0, 0.12)",
"focusOpacity": 0.12,
"activatedOpacity": 0.12,
"activeChannel": "0 0 0",
"selectedChannel": "0 0 0"
}
}

Customize at your own risk!