Modern Industrial System is a production-grade React component library built to prove a specific thesis: that a design system should have a strong opinion, not just a colour palette. I engineered 20 components across four categories — Foundations, Navigation, Data & Feedback, and Complex Units — with a hybrid Tailwind + CSS Modules architecture, a 5-token theme contract that powers a full runtime theme switch, and an accessibility baseline that treats focus management as non-negotiable, not an afterthought.

Every architectural choice was made to serve the system's primary constraint: it has to look like it came from one place.
| Framework | Next.js 16 App Router with full TypeScript throughout. Every component prop surface is typed — no implicit any, no escape hatches. The showcase page is a single client component; all 20 library components are fully server-renderable by default. |
| Styling | CSS Modules + Tailwind v4 A deliberate hybrid. Tailwind v4 handles layout and spacing utilities. CSS Modules own all animation and interaction code — shimmer keyframes, scan-line effects, custom slider thumbs — where scoping and performance matter. The rule: Tailwind for the Standard, CSS Modules for the Craft. |
| Tokens | 5-Token Theme Contract Every themeable component references exactly five semantic variables: --accent, --on-accent, --bg-surface, --border-subtle, and --radius. Switching themes means redefining those five values under a [data-theme] selector. The entire UI repaints in one attribute change. |
| Primitives | Radix UI (selective) Used only where the accessibility contract is genuinely complex — Dialog, Accordion, Tooltip. Simpler components (Button, Input, Badge, Checkbox) are hand-rolled, because a library that delegates everything to Radix teaches you nothing about how those patterns actually work. |
| Architecture | 4px Grid + Focus Contract Every spacing value in the system is a multiple of 4px, defined as a CSS custom property. Every interactive element ships with a distinct focus-visible state. These are enforced at the token level, not by convention — breaking them requires actively fighting the system. |
The constraint wasn't the components. It was the animation system.
CSS Modules create isolated scopes — which is exactly what you want for class names. What's less obvious is that @keyframes defined in a global stylesheet are not reliably inherited inside a .module.css file in Next.js with Turbopack. The shimmer animation on the Skeleton component, the slide-up on Toast, the spin on Button's loading state — all of them silently failed because they referenced keyframe names that existed in globals.css but weren't visible inside the module's scope.
The failure mode was invisible in development but consistent in production: the component rendered correctly, the CSS class applied, but the animation simply didn't run. No error, no warning — just a static element where motion was expected.
The fix was a rule: every .module.css file is a self-contained unit. If a file uses an animation name, the @keyframes block lives in that same file. No cross-file keyframe references, ever. This required auditing all 20 component stylesheets and adding local keyframe definitions to eight of them — shimmer in Skeleton,toastSlideUp and toastFadeOut in Toast, fadeIn andscaleIn in Modal and CommandPalette, progressFill in ProgressBar, and a renamed buttonSpin (not spin) in Button to avoid any possible collision with global scope.
/* Each .module.css defines its own keyframes — no globals dependency */
.spinnerRing {
animation: buttonSpin 0.7s linear infinite; /* local name */
}
@keyframes buttonSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}Hindsight is the best code reviewer.
The theme contract works well with five tokens, but it exposes a gap: semantic colour tokens and component-specific tokens are mixed in the same :root block. A cleaner architecture would separate them into two layers — a primitive layer (--color-orange-500: #FF4D00) and a semantic layer (--accent: var(--color-orange-500)) — so that adding a third theme doesn't require hunting through component stylesheets for hardcoded values. The current system works, but it required an audit pass to catch every stray #FF4D00 when the punk theme was added.
I'd also reach for CSS custom properties on individual elements earlier for animation state. The ProgressBar's fill animation uses a --progress-value property set inline on the element — that pattern is clean and avoids JavaScript-driven style injection entirely. I'd apply the same approach to the RangeSlider fill position, which currently uses a calculated inline width style rather than a custom property, making it harder to animate smoothly with pure CSS.
Finally: the Command Palette's CtrlK shortcut required { capture: true } on the event listener to intercept the browser's default behaviour before it claimed the keystroke. That's a non-obvious fix, and it means the component has a hidden dependency on listener registration order. The cleaner solution is a dedicated hotkey manager — a single global listener that other components register with, rather than each component managing its own window.addEventListener call independently.