How It Works
Workflow
1. Custom Variables
The src/styles/config folder contains all the color, spacing, font sizes, border radii, breakpoints & styling tokens needed for all libraries to have a consistent design.
The variables that are needed for library configurations are defined via the --custom-* prefix inside a :root { ... } OR inside the .dark { ... } class (This is used by Tailwind CSS & Shadcn to apply dark theme styles).
/* colors-light.css */
:root {
--custom-color-primary: #357a6e;
--custom-color-background: #f9f6f2;
}
/* colors-dark.css */
.dark {
--custom-color-primary: #e58e6d;
--custom-color-background: #1c1a18;
}This project's config files include:
| File | What it defines |
|---|---|
colors/colors-light.css | Color for light mode. |
colors/colors-dark.css | Color for dark mode (same token names as). |
colors/colors.css | Other custom color tokens (e.g. shadows) OR applying default / base colors to standard HTML tags. |
fonts/fonts.css | Font sizes (including per breakpoint variants), weight, style, etc. |
borders.css | Border and outline colors, sizes, etc. |
measurements.css | Spacing and layout dimension tokens (padding, margins, heights, widths, etc.). |
breakpoints.css | Breakpoint values |
2. Code Generation
During development, if you change a value in src/styles/config/, four scripts are automatically run to generate files that assist in the configuration of other libaries.
The Scripts
| Script | What it does |
|---|---|
generateTokens | Reads every config file and exports all --custom-* values as a TypeScript object |
generateTailwindBreakpoints | Copies breakpoint values as plain pixel strings |
generateColorsRgb | Converts every color to an RGB triplet |
generateFontsResponsive | Turns breakpoint-scoped font size tokens into @media overrides |
The Generated Files
tokens.ts
All --custom-* values exported as a typed TypeScript object.
The number value is useful when a library requires a numeric input (e.g. MUI's fontSize).
export const tokens = {
// Color tokens — plain hex strings
light: {
colorPrimary: "#357a6e",
colorBackground: "#f9f6f2",
// ...
},
dark: {
colorPrimary: "#e58e6d",
colorBackground: "#1c1a18",
// ...
},
// Non-color tokens — { string, number } shape
radiusBase: { string: "0.625rem", number: 10 },
breakpointMd: { string: "900px", number: 900 },
fontSizeH1: { string: "2.5rem", number: 40 },
// ...
};tailwind-breakpoints.css
Breakpoint values as literal px strings for Tailwind's @theme block (Tailwind v4 doesn't support var() in breakpoints):
@theme inline {
--breakpoint-xs: 0px;
--breakpoint-sm: 600px;
--breakpoint-md: 900px;
--breakpoint-lg: 1200px;
--breakpoint-xl: 1536px;
}colors-rgb.css
Every color token as an RGB triplet, needed by React Bootstrap's internal rgba() calculations:
:root {
--custom-color-primary-rgb: 53, 122, 110;
--custom-color-background-rgb: 249, 246, 242;
/* ... */
}fonts-responsive.css
@media overrides that swap base font size tokens at each breakpoint:
@media (max-width: 899px) {
:root {
--custom-font-size-h1: var(--custom-font-size-h1-md);
--custom-font-size-body: var(--custom-font-size-body-md);
/* ... */
}
}3. Library Configurations
Once the generated files are in place, each library reads from them in TWO ways:
1. CSS variables
Tailwind & Shadcn and React Bootstrap map --custom-* vars directly into each library's own variable names.
/* shadcn.css */
@theme inline {
--color-primary: var(--custom-color-primary);
}
/* reactbootstrap.css */
:root {
--bs-primary: var(--custom-color-primary);
--bs-primary-rgb: var(--custom-color-primary-rgb);
}2. TypeScript Tokens
MUI and Ant Design are configured in TypeScript, so they use the generated tokens object to configure their themes.
import { tokens } from '@/src/styles/_generated/tokens'
// muiTheme.ts
createTheme({
colorSchemes: {
light: { palette: { primary: { main: tokens.light.colorPrimary } } },
dark: { palette: { primary: { main: tokens.dark.colorPrimary } } },
},
});
// antdTheme.ts
export const antdLightTheme: ThemeConfig = {
token: { colorPrimary: tokens.light.colorPrimary },
};How They Work Together
CSS Layering
All CSS is imported into src/styles/app.css, which declares an explicit layer order at the top:
@layer tailwind-theme, tailwind-preflight, shadcn-overrides, bootstrap, antd, reactbootstrap, base, mui, tailwind-components, tailwind-utilities;The priority order is from least (left) to most importent (right).
This allows libraries to correctly override default setup but also allows custom (base layer) setup to have priority for a more consistent design. See Pain Points: React Bootstrap Global Styles for why this layer ordering was necessary.
Providers & Syncers
Most React UI libraries needs a Provider to apply its theme. All providers are consolidated into a CombinedThemeProvider, which is applied once in the root layout.
type CombinedThemeProviderProps = {
children: ReactNode;
initialTheme: "light" | "dark";
};
export const CombinedThemeProvider = ({
children,
initialTheme,
}: CombinedThemeProviderProps) => {
return (
<NextThemesProvider
attribute={"class"}
enableSystem
defaultTheme="system"
disableTransitionOnChange
>
<AntdThemeProvider initialTheme={initialTheme}>
<MuiThemeProvider>
{/* Syncers explained in the "Light & Dark Mode" section*/}
<ReactBootstrapThemeSyncer>
{children}
</ReactBootstrapThemeSyncer>
</MuiThemeProvider>
</AntdThemeProvider>
</NextThemesProvider>
);
};Light & Dark Mode
This project uses next-themes to manage the user's theme preference. It persists
the choice to localStorage and toggles a dark class on <html> when dark mode
is active.
dark Class
Tailwind, MUI and Shadcn respond to the dark class automatically. Custom color
tokens are defined in :root and overridden in dark, so no extra config is needed.
Syncers
A Syncer is a small client component that uses const { resolvedTheme } = useTheme() to listen for changes in the user's preferred theme choice.
It is needed when:
- It cannot access the
darkclass directly - It has its own light / dark mode state that needs to be in sync with the user's preference
| Library | When the theme is changed |
|---|---|
| MUI | Calls setMode() to keep MUI's internal state in sync |
| React Bootstrap | Sets the data-bs-theme attribute on the <html> |
| Ant Design | Sets the theme-resolved cookie then passes it into <ConfigProvider> — see Pain Points: SSR Flicker |
Initial Injection Scripts & Cookies
For MUI and React Bootstrap, a script is added to set the get the user's preferred theme before the page is served.
For Ant Design, a cookie is read to get the user's preferred theme before the page is served.
This is done to remove a flicker when loading the page. See Pain Points: SSR Flicker for details.
