Pain Points
Real problems encountered while building this project, and how they were solved.
Workarounds
Issue: Tailwind v4 Doesn't Support var() in Breakpoints
Tailwind v4's @theme block only accepts literal values.
var() references are not resolved at build time.
Since breakpoints live as CSS variables in breakpoints.css, they can't be passed to @theme directly.
/* breakpoints.css */
:root {
--custom-breakpoint-md: 900px;
}
/* tailwind.css */
@theme inline {
/* ❌ Does not work in Tailwind v4 */
--breakpoint-md: var(--custom-breakpoint-md);
}Solution: A script to automatically generate literal breakpoints in Tailwind
The generateTailwindBreakpoints script generates each breakpoint variable to its literal value and writes a ready-to-use @theme inline {} block.
/* tailwind-breakpoints.css (generated)' */
@theme inline {
--breakpoint-xs: 0px;
--breakpoint-sm: 600px;
--breakpoint-md: 900px;
--breakpoint-lg: 1200px;
--breakpoint-xl: 1536px;
}See How It Works for the full codegen pipeline.
Issue: @media Queries Don't Support var() in Conditions
Similar to the issue above, this is a known CSS limitation.
@media (max-width: var(--custom-breakpoint-md)) does not work.
This means the responsive font overrides in fonts.css would need hardcoded pixel values, duplicating breakpoint values already defined in breakpoints.css and breaking the single source of truth.
/* fonts.css */
:root {
--custom-font-size-h1: 2.5rem;
--custom-font-size-h1-md: 2rem;
}
/* ❌ Does not work — var() not supported in @media conditions */
@media (max-width: var(--custom-breakpoint-md)) { ... }
/* ✅ Works syntactically */
@media (max-width: 899px) { ... }
/* ⚠️ But hardcodes 899px — duplicates the value already in breakpoints.css */Solution: A script to automatically generate the media queries for each known breakpoint and font size
The generateFontsResponsive script reads the breakpoint-scoped font tokens from fonts.css, pairs them with the resolved breakpoint values from breakpoints.css, and emits @media overrides that reassign the base tokens at each breakpoint.
/* fonts-responsive.css (generated) - literal values come from 'breakpoint.css */
@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);
}
}
@media (max-width: 599px) {
:root {
--custom-font-size-h1: var(--custom-font-size-h1-sm);
--custom-font-size-body: var(--custom-font-size-body-sm);
}
}See How It Works for the full codegen pipeline.
Issue: React Bootstrap Global Styles Overriding Other Libraries
Bootstrap ships with aggressive global styles — base resets, font overrides, and !important on some component styles. These were bleeding across the whole app, overriding Tailwind utilities and breaking Shadcn and MUI components.
/* Bootstrap internals */
.btn {
--bs-btn-font-size: 1rem;
}
.btn-close {
background: transparent url("...") !important;
}Solution: CSS Layer Order
Declaring an explicit layer order in app.css means later layers always win — regardless of specificity or !important.
@layer tailwind-theme, tailwind-preflight, shadcn-overrides,
bootstrap, antd, reactbootstrap,
base, mui, tailwind-components, tailwind-utilities;Bootstrap is declared early, so base, mui, and tailwind-utilities always override it. See How It Works: CSS Layering for more detail.
Issue: SSR Flicker
next-themes stores the user's theme preference in localStorage which is unavailable server-side.
On first load, the page will show the default theme. If it does not match the user's preference, it will switch.
This short delay can cause a flicker on the screen.
Solution: Injection Scripts (MUI & React Bootstrap)
Both libraries respond to an HTML attribute set before React hydrates. Inline scripts in layout.tsx read localStorage and set the correct attribute synchronously — before any paint.
MUI ships InitMuiColorSchemeScript, which injects a blocking script that sets the dark class on <html>:
// layout.tsx
<InitMuiColorSchemeScript attribute="class" defaultMode="system" />React Bootstrap reads data-bs-theme, so a custom script handles it:
// layout.tsx
<script
dangerouslySetInnerHTML={{
__html: `
try {
const stored = localStorage.getItem('theme')
const system = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
const theme = stored === 'dark' || stored === 'light' ? stored : system
document.documentElement.setAttribute('data-bs-theme', theme)
} catch(e) {}
`,
}}
/>Ant Design Solution: Cookie Sync
Ant Design applies its theme through CSS-in-JS (ConfigProvider), not an HTML attribute. There's no CSS hook for it to read at render time, so the injection script approach doesn't apply.
AntdThemeProvider writes the resolved theme to a cookie on the client. layout.tsx reads it server-side and passes initialTheme into AntdThemeProvider before the first render.
// AntdThemeProvider.tsx
useEffect(() => {
document.cookie = `theme-resolved=${resolvedTheme}; path=/`;
}, [resolvedTheme]);
// layout.tsx (server)
const cookieTheme = (await cookies()).get("theme-resolved")?.value;
const initialTheme = cookieTheme === "dark" ? "dark" : "light";
return (
<CombinedThemeProvider initialTheme={initialTheme}>
{children}
</CombinedThemeProvider>
);This is a known limitation of next-themes v0.4.6 — no native cookie support. See next-themes Native Cookie Support for the planned improvement.
Issue: React Bootstrap Needs RGB Color Triplets
Bootstrap uses rgba() internally for focus rings, disabled states, and overlays.
It expects separate --bs-*-rgb triplets rather than just hex values.
/* An example from 'bootstrap/dist/css/bootstrap.css' */
.btn-primary:focus-visible {
/* ❌ Without a source for --bs-primary-rgb, this breaks */
box-shadow: 0 0 0 0.25rem rgba(var(--bs-primary-rgb), 0.5);
}Solution: A script to generate RGB equivalents of custom colors
The generateColorsRgb script converts every --custom-color-* hex value to an RGB triplet and writes both light and dark sets to colors-rgb.css.
reactbootstrap.css maps these to the --bs-*-rgb names Bootstrap expects.
/* colors-rgb.css (generated) */
:root {
--custom-color-primary-rgb: 53, 122, 110;
--custom-color-background-rgb: 249, 246, 242;
}
.dark {
--custom-color-primary-rgb: 229, 142, 109;
--custom-color-background-rgb: 28, 26, 24;
}
/* reactbootstrap.css */
:root {
--bs-primary-rgb: var(--custom-color-primary-rgb);
}See React Bootstrap: RGB Triplets for the full setup.
Future Improvements
next-themes Native Cookie Support
If next-themes adds cookie-based storage, the manual workaround in AntdThemeProvider and layout.tsx can be removed entirely.
See SSR Flicker for the current workaround.
Single Codegen Pass
The four scripts (generateTokens, generateTailwindBreakpoints, generateColorsRgb, generateFontsResponsive) each parse the config files independently.
A single pipeline pass would be faster and simpler. See How It Works: Code Generation for the current setup.
More Frontend Libraries
Next up on the list are Chakra and Mantine.
Limitations
Missing Configurations
This project only scratches the surface of unified theming. The main design consistency achieved was syncing the colors, font sizes, font weight, borders, etc.
But popular frontend libraries have hundreds of unknown CSS configurations that would be time consuming to fully conslidate between multiple libraries.
