Docusaurus is an excellent tool for building documentation websites. However, it does not support WindiCSS out of the box. Mantine is a React UI library that provides a lot of UI components and hooks, so you can use them to build your components.
This guide will show you how to integrate WindiCSS and Mantine with Docusaurus.
Getting Startedβ
We will go through setting up these features:
- β
WindiCSS
- β Dark mode
- β Typography
- β Animations (from @windicss/plugin-animations)
- β Attributify mode
- β Colors
- β Design in DevTools mode
- β
Mantine
- β Dark mode
- β Typography
- β Colors
Currently, there are some bugs with WindiCSS Devtool in windicss-webpack-plugin package, which are reported in #118 and #115.
Setup Docusaurusβ
First, we need to set up a Docusaurus project. You can follow the official guide from Docusaurus website:
pnpm create docusaurus demo-docs classic
Then, we have this project structure:
.
βββ babel.config.js
βββ blog
βββ docs
βββ docusaurus.config.js
βββ node_modules
βββ package.json
βββ pnpm-lock.yaml
βββ README.md
βββ sidebars.js
βββ src
β βββ components
β βββ css
β βββ pages
βββ static
βββ img
Setup WindiCSSβ
Install dependenciesβ
We will have to install windicss and windicss-webpack-plugin package:
pnpm add -D windicss windicss-webpack-plugin
Configure Webpackβ
Following the official guide from WindiCSS, we have to configure Webpack to use the windicss-webpack-plugin.
Because the file webpack.config.ts somehow is not loaded by Docusaurus, we
have to configure Webpack manually in the file docusaurus.config.js by
creating a custom Docusaurus
plugin:
const WindiCSSWebpackPlugin = require('windicss-webpack-plugin');
/** @type {import('@docusaurus/types').Config} */
const config = {
plugins: [
function windicssPlugin() {
return {
name: 'windicss-plugin',
configureWebpack() {
return {
plugins: [new WindiCSSWebpackPlugin()],
};
},
};
},
],
};
module.exports = config;
Include the virtual moduleβ
It's recommended to include the WindiCSS virtual module within an entry point file or something only loaded once.
However, Docusaurus does not have an entry file like index.js or index.ts,
so we have to "swizzle" the Root
component by creating a wrapper around it to import the module.
The
<Root>component is rendered at the very top of the React tree, above the theme<Layout>, and never unmounts. It is the perfect place to add stateful logic that should not be re-initialized across navigations (user authentication status, shopping card state...).
You should aware that the Root component is not mentioned whether it is
Safe or Unsafe by running the command pnpm swizzle.
Import all three layers:
import React from 'react';
import 'windi.css';
// Default implementation, that you can customize
export default function Root({ children }: { children?: React.ReactNode }) {
return <>{children}</>;
}
Because the file windi-base.css overrides the default styles of
Docusaurus, so I recommend not importing it:
import React from 'react';
-import 'windi.css';
+import 'windi-components.css';
+import 'windi-utilities.css';
// Default implementation, that you can customize
export default function Root({ children }: { children?: React.ReactNode }) {
return <>{children}</>;
}
Test Locallyβ
Now, we can test the result by writing some classes in HomepageHeader
the component in the file src/pages/index.tsx:
// ...
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className="bg-blue-500">
<div className="container mx-auto text-center py-24">
<h1 className="text-4xl font-bold text-white">{siteConfig.title}</h1>
<p className="text-xl py-6 text-white">{siteConfig.tagline}</p>
<div className="py-10">
<Link
className="bg-white rounded-md text-gray-500 px-4 py-2"
to="/docs/intro"
>
Docusaurus Tutorial - 5min β±οΈ
</Link>
</div>
</div>
</header>
);
}
// ...
And we can see the result:

Configure WindiCSSβ
You can configure WindiCSS by creating a file windi.config.ts in the root
directory.
Dark Modeβ
Windi CSS has out-of-box Dark Mode support.
By prefixing the
dark:variant to utilities, they will only apply when dark mode is enabled.We have two modes for enabling dark mode, class mode and media query mode. By default, class mode is enabled.
Enable dark mode using class mode:
export default {
darkMode: 'class',
};
To manually add class dark to the <html> element, we can add some logic in
the Layout/Provider wrapper:
- Add or remove the class
darkbased on the color scheme.
// eslint-disable-next-line import/no-extraneous-dependencies
import { useColorMode } from '@docusaurus/theme-common';
import { ColorSchemeProvider } from '@mantine/core';
import Provider from '@theme-original/Layout/Provider';
import React, { useEffect } from 'react';
import { MantineProvider } from '@site/src/context/MantineProvider';
const CustomProvider = ({ children }: { children?: React.ReactNode }) => {
const { colorMode, setColorMode } = useColorMode();
useEffect(() => {
if (colorMode === 'dark') {
document.documentElement.classList.add('dark');
} else if (colorMode === 'light') {
document.documentElement.classList.remove('dark');
}
}, [colorMode]);
return <>{children}</>;
};
export default function ProviderWrapper({
children,
...props
}: {
children?: React.ReactNode;
}) {
return (
<Provider {...props}>
<CustomProvider>{children}</CustomProvider>
</Provider>
);
}
The useColorMode hook MUST be called inside the ProviderWrapper component.
That's why I have to create a separate component CustomProvider to wrap the
useColorMode hook.
Another approach by swizzling the ColorModeToggle component
This approach is considered simpler than the previous one because the color mode
is already passed to the ColorModeToggle component.
The ColorModeToggle component is Safe to swizzle:
import ColorModeToggle from '@theme-original/ColorModeToggle';
import type { Props } from '@theme/ColorModeToggle';
import React, { useEffect } from 'react';
export default function ColorModeToggleWrapper(props: Props) {
const { value: colorMode } = props;
useEffect(() => {
if (colorMode === 'dark') {
document.documentElement.classList.add('dark');
} else if (colorMode === 'light') {
document.documentElement.classList.remove('dark');
}
}, [colorMode]);
return (
<>
<ColorModeToggle {...props} />
</>
);
}
This approach only works when the color mode switch is enabled. If you disable
the button by setting themeConfig.colorMode.disableSwitch to true in the
file docusaurus.config.js, the ColorModeToggle component will not be
rendered.
Why don't we use hook
useColorMode?Yes, you can, but the color mode is already passed to the component as a prop, we can use it directly.
Typographyβ
WindiCSS font family utilities (font-sans, font-serif, font-mono)
configured fonts are quite different from the ones configured in Docusaurus. So
we can configure the fonts in windi.config.ts to match with Docusaurus
configurations:
- Font family base from variable
--ifm-font-family-base. - Font family mono from variable
--ifm-font-family-monospace. - Font family serif is not present in Docusaurus, so we can use the default value.
export default {
theme: {
extend: {
fontFamily: {
sans: 'system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
mono: 'SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace',
},
},
},
};
Animationsβ
This section is optional. WindiCSS has built-in animations
support since v3.1.
To add quick animations to your site, you can use WindiCSS's animation default
utilities, but it's quite limited. So we can install the plugin
@windicss/plugin-animations to add more animations utilities from Animate
CSS
Install the plugin:
pnpm add -D @windicss/plugin-animations
Add the plugin to windi.config.ts:
import pluginAnimations from '@windicss/plugin-animations';
export default {
plugins: [
// Other plugins
pluginAnimations({
settings: {
// animatedSpeed: 1000,
},
}),
],
};
Then you can use the animation utilities:
// ...
function HomepageHeader() {
return (
<p className="animate-animated animate-infinite animate-rubberBand">
Hello World
</p>
);
}
// ...
Attributify Modeβ
To configure the attributify mode, we can configure it in windi.config.ts:
export default {
attributify: true,
};
Then you can use the WindiCSS utilities as HTML attributes:
// ...
function HomepageHeader() {
return (
<button
bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"
text="sm white"
font="mono light"
p="y-2 x-4"
border="2 rounded blue-200"
>
Button
</button>
);
}
// ...
Since Mantine also provides some default
props for all
components, like: p, px, py, m, mx, my,... so if you use the
attributify mode, you should add a prefix to the WindiCSS utilities to avoid
conflicts. For example, you can setup the prefix w: for WindiCSS utilities:
export default {
attributify: {
prefix: 'w:',
},
};
Then you can use the WindiCSS utilities as HTML attributes:
// ...
function HomepageHeader() {
return (
<button
w:bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"
w:text="sm white"
w:font="mono light"
w:p="y-2 x-4"
w:border="2 rounded blue-200"
>
Button
</button>
);
}
// ...
Colorsβ
Docusaurus configured primary colors from src/css/custom.css:
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
We can configure WindiCSS to use these in class as well:
export default {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#2e8555',
dark: '#29784c',
darker: '#277148',
darkest: '#205d3b',
light: '#33925d',
lighter: '#359962',
lightest: '#3cad6e',
},
'dark-primary': {
DEFAULT: '#25c2a0',
dark: '#21af90',
darker: '#1fa588',
darkest: '#1a8870',
light: '#29d5b0',
lighter: '#32d8b4',
lightest: '#4fddbf',
},
},
},
},
};
Then you can use Docusaurus primary colors in class:
// ...
function HomepageHeader() {
return (
<p className="bg-primary hover:bg-primary-dark dark:bg-dark-primary dark:hover:bg-dark-primary-dark">
Hello World
</p>
);
}
// ...
Setup Mantineβ
Install dependenciesβ
pnpm add @mantine/core @mantine/hooks @emotion/react
Configure Mantine Providerβ
In this step, we will add Mantine Provider to the <Root> to
ensure that all Mantine components are using the same theme.
+import { MantineProvider } from '@mantine/core';
import React from 'react';
// eslint-disable-next-line import/no-unresolved
import 'windi-components.css';
// eslint-disable-next-line import/no-unresolved
import 'windi-utilities.css';
// Default implementation, that you can customize
export default function Root({ children }: { children?: React.ReactNode }) {
return (
- <>{children}</>
+ <MantineProvider>{children}</MantineProvider>
);
}
This is just a basic setup, we will use our custom <MantineProvider> in the
next step.
Custom Mantine Providerβ
In this file, we will configure the Mantine Provider to use the WindiCSS theme (colors, fonts, breakpoints, etc) and Docusaurus theme (colors, fonts, etc), so we can ensure the consistency between the Docusaurus site, Mantine components, and WindiCSS utilities.
import {
MantineProvider as BaseMantineProvider,
Global,
MantineTheme,
DEFAULT_THEME as mantineDefaultTheme,
} from '@mantine/core';
import type { MantineSizes } from '@mantine/core';
import React from 'react';
import windiDefaultColors from 'windicss/colors';
import windiDefaultTheme from 'windicss/defaultTheme';
import type { DefaultColors } from 'windicss/types/config/colors';
import type { DefaultFontSize, ThemeType } from 'windicss/types/interfaces';
import type { MantineThemeColors } from '@site/src/types/MantineThemeColors';
const convertBreakpoint = (breakpoint: ThemeType): MantineSizes => {
const convertedBreakpoint = {} as MantineSizes;
Object.keys(breakpoint).forEach((size) => {
// NOTE: Have to remove 'px' from breakpoint and convert to number
convertedBreakpoint[size] = +breakpoint[size].replace('px', '');
});
return convertedBreakpoint;
};
// Override Mantine colors
const convertColor = (windiColors: DefaultColors) => {
const convertedColor = {} as MantineThemeColors;
Object.keys(windiColors).forEach((color) => {
if (color === 'lightBlue') {
color = 'sky';
} else if (color === 'warmGray') {
color = 'stone';
} else if (color === 'trueGray') {
color = 'neutral';
} else if (color === 'coolGray') {
color = 'gray';
} else if (color === 'blueGray') {
color = 'slate';
} else if (color === 'zink') {
color = 'zinc';
}
if (windiColors[color] instanceof Object) {
convertedColor[color] = Object.values(windiColors[color]);
}
});
// NOTE: WindiCSS dark color is too dark
convertedColor.dark = convertedColor.zinc;
return convertedColor;
};
const convertFontSize = (fontSize: {
[key: string]: DefaultFontSize;
}): MantineSizes => {
const convertedFontSize = {} as MantineSizes;
Object.keys(fontSize).forEach((size) => {
// NOTE: Don't have to convert 'rem' to 'px'
convertedFontSize[size] = fontSize[size][0];
});
return convertedFontSize;
};
const theme: MantineTheme = {
...mantineDefaultTheme,
breakpoints: {
...mantineDefaultTheme.breakpoints,
...convertBreakpoint(windiDefaultTheme.screens), // WindiCSS
},
colors: {
...mantineDefaultTheme.colors,
...convertColor(windiDefaultColors),
},
defaultRadius: 'md',
black: windiDefaultColors.black as string,
white: windiDefaultColors.white as string,
primaryColor: 'blue',
fontSizes: {
...mantineDefaultTheme.fontSizes,
...convertFontSize(windiDefaultTheme.fontSize),
},
radius: {
...mantineDefaultTheme.radius,
// NOTE: WindiCSS border radius messed up with Mantine
// ...windiDefaultTheme.borderRadius,
},
fontFamily:
'system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
fontFamilyMonospace:
'SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace',
headings: {
...mantineDefaultTheme.headings,
fontFamily:
'system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
},
lineHeight: mantineDefaultTheme.lineHeight,
loader: 'oval',
shadows: {
...mantineDefaultTheme.shadows,
...windiDefaultTheme.boxShadow,
},
};
const MyGlobalStyles = () => {
return (
<Global
styles={{
'html.dark': {
img: {
filter: 'brightness(.8) contrast(1.2)',
},
},
}}
/>
);
};
type MantineProps = {
children: React.ReactNode;
theme?: Partial<MantineTheme>;
};
const MantineProvider = ({
children,
theme: themeProps,
...props
}: MantineProps) => {
return (
<BaseMantineProvider theme={{ ...theme, ...themeProps }} {...props}>
<MyGlobalStyles />
{children}
</BaseMantineProvider>
);
};
export { MantineProvider };
Use our custom <MantineProvider> in src/theme/Layout/Provider/index.tsx:
import { useColorMode } from '@docusaurus/theme-common';
import Provider from '@theme-original/Layout/Provider';
import React, { useEffect } from 'react';
+import { MantineProvider } from '@site/src/context/MantineProvider';
const CustomProvider = ({ children }) => {
const { colorMode, setColorMode } = useColorMode();
useEffect(() => {
if (colorMode === 'dark') {
document.documentElement.classList.add('dark');
} else if (colorMode === 'light') {
document.documentElement.classList.remove('dark');
}
}, [colorMode]);
- return <>{children}</>;
+ return (
+ <MantineProvider>
+ {children}
+ </MantineProvider>
+ );
};
export default function ProviderWrapper({ children, ...props }) {
return (
<Provider {...props}>
<CustomProvider>{children}</CustomProvider>
</Provider>
);
}
Configure Mantineβ
Dark Modeβ
We will pass the color mode to Mantine using the ColorSchemeProvider component
and theme.colorScheme props:
import { useColorMode } from '@docusaurus/theme-common';
+import { ColorSchemeProvider } from '@mantine/core';
import Provider from '@theme-original/Layout/Provider';
import React, { useEffect } from 'react';
+import { MantineProvider } from '@site/src/context/MantineProvider';
const CustomProvider = ({ children }) => {
const { colorMode, setColorMode } = useColorMode();
useEffect(() => {
if (colorMode === 'dark') {
document.documentElement.classList.add('dark');
} else if (colorMode === 'light') {
document.documentElement.classList.remove('dark');
}
}, [colorMode]);
+ const toggleColorScheme = (value) =>
+ setColorMode(value || (colorMode === 'dark' ? 'light' : 'dark'));
- return (
- <MantineProvider>
- {children}
- </MantineProvider>
- );
+ return (
+ <ColorSchemeProvider
+ colorScheme={colorMode}
+ toggleColorScheme={toggleColorScheme}
+ >
+ <MantineProvider theme={{ colorScheme: colorMode }}>
+ {children}
+ </MantineProvider>
+ </ColorSchemeProvider>
+ );
};
export default function ProviderWrapper({ children, ...props }) {
return (
<Provider {...props}>
<CustomProvider>{children}</CustomProvider>
</Provider>
);
}
So now we can use the useMantineColorScheme (or useColorMode from
Docusaurus) hook to get the color mode:
// ...
import { Button, useMantineColorScheme } from '@mantine/core';
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark';
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/intro"
>
Docusaurus Tutorial - 5min β±οΈ
</Link>
</div>
<Button
variant="outline"
onClick={() => toggleColorScheme()}
title="Toggle color scheme"
>
{dark ? 'dark' : 'light'}
</Button>
</div>
</header>
);
}
// ...
The useMantineColorScheme (or useColorMode) hook MUST be called inside the
Layout component.
Typographyβ
We will have to convert font sizes from WindiCSS types to Mantine types, apply font-family (from Docusaurus) and line-height:
For example:
| WindiCSS sample | Mantine sample |
|---|---|
| |
// ...
import {
MantineTheme,
DEFAULT_THEME as mantineDefaultTheme,
} from '@mantine/core';
import type { MantineSizes } from '@mantine/core';
import { DEFAULT_THEME as mantineDefaultTheme } from '@mantine/core';
import windiDefaultTheme from 'windicss/defaultTheme';
import type { DefaultFontSize } from 'windicss/types/interfaces';
const convertFontSize = (fontSize: {
[key: string]: DefaultFontSize;
}): MantineSizes => {
const convertedFontSize = {} as MantineSizes;
Object.keys(fontSize).forEach((size) => {
// NOTE: Don't have to convert 'rem' to 'px'
convertedFontSize[size] = fontSize[size][0];
});
return convertedFontSize;
};
const theme: MantineTheme = {
...mantineDefaultTheme,
fontSizes: {
...mantineDefaultTheme.fontSizes,
...convertFontSize(windiDefaultTheme.fontSize),
},
fontFamily:
'system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
fontFamilyMonospace:
'SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace',
headings: {
...mantineDefaultTheme.headings,
fontFamily:
'system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"',
},
lineHeight: mantineDefaultTheme.lineHeight,
};
// ...
Colorsβ
At this step, we have to convert the WindiCSS color types to Mantine color types and remove unused colors.
We will use the default Mantine dark color, instead of WindiCSS dark color
for better contrast. Moreover, we will set primaryShade to 7.
For example:
| WindiCSS sample | Mantine sample |
|---|---|
| |
// ...
import {
MantineTheme,
DEFAULT_THEME as mantineDefaultTheme,
} from '@mantine/core';
import windiDefaultColors from 'windicss/colors';
import type { DefaultColors } from 'windicss/types/config/colors';
import type { MantineThemeColors } from '@site/src/types/MantineThemeColors';
// Override Mantine colors
const convertColor = (windiColors: DefaultColors) => {
const convertedColor = {} as MantineThemeColors;
Object.keys(windiColors).forEach((color) => {
if (color === 'lightBlue') {
color = 'sky';
} else if (color === 'warmGray') {
color = 'stone';
} else if (color === 'trueGray') {
color = 'neutral';
} else if (color === 'coolGray') {
color = 'gray';
} else if (color === 'blueGray') {
color = 'slate';
} else if (color === 'zink') {
color = 'zinc';
}
if (windiColors[color] instanceof Object) {
convertedColor[color] = Object.values(windiColors[color]);
}
});
// NOTE: WindiCSS dark color is too dark
convertedColor.dark = mantineDefaultTheme.colors.dark;
return convertedColor;
};
const theme: MantineTheme = {
...mantineDefaultTheme,
colors: {
...mantineDefaultTheme.colors,
...convertColor(windiDefaultColors),
},
black: windiDefaultColors.black as string,
white: windiDefaultColors.white as string,
primaryColor: 'blue',
primaryShade: 7,
};
// ...
