Component Architecture
This section explains how the Equality UI library is structured under the hood — how components use Tailwind, CSS Modules, and the theme system to stay fully scoped, reusable, and conflict-free.
Overview
Each UI component in the library is built to be:
- Self-contained – no global CSS imports or leaks
- Themed – all tokens resolve via the active theme and Tailwind tokens
- Composable – extendable through the
classNameprop
Internally, every component follows this structure:
src/
components/
button/
button.tsx
button.module.css
CSS Modules + Tailwind
Every component imports its own .module.css file. Inside that file, Tailwind utilities are applied using the @apply directive.
@reference '../../theme/theme.module.css';
.button {
@apply ring-offset-background rounded-md;
@apply text-sm font-medium whitespace-nowrap;
@apply transition-all duration-300;
@apply inline-flex items-center justify-center gap-1;
@apply focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden;
@apply disabled:cursor-not-allowed disabled:opacity-50;
@apply [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0;
}
The @tailwindcss/postcss plugin processes these files during build, generating scoped class names automatically (e.g. _button_container__abc123).
And then it’s used like this:
import styles from "./button.module.css";
export function Button({ className, ...props }) {
return <button className={cn(styles["button"], className)} {...props} />;
}
Because Tailwind runs at build time, there’s no runtime overhead — just static CSS.
Theme system
At the root level, the theme color tokens are imported via the @eqtylab/equality/theme-config.css import or the <ThemeProvider>:
--color-primary: hsl(210 40% 98%);
--color-foreground: hsl(240 4% 16%);
--text-xxs: 0.625rem;
Tailwind utilities in the components reference these tokens, so bg-primary maps directly to --color-primary.
When you change the theme via
import { useTheme } from "@eqtylab/equality";
const [theme, setTheme] = useTheme();
setTheme("dark");
…the provider updates those variables, and every component automatically restyles itself — no rebuilds, no extra logic.
Build process
The library uses Vite in library mode (or tsup if preferred) to compile everything cleanly:
- TypeScript builds the component logic.
- PostCSS + Tailwind transform
.module.cssfiles. - CSS Modules scope the output so class names never collide.
- Theme tokens are exported as a Tailwind configuration file and also embedded and exposed through a
<ThemeProvider />to allow for integration in various frameworks. - Output goes into
/distas both ESM and CJS modules.
This setup means host apps can import components without any Tailwind setup of their own if they require.
Layered isolation
| Layer | Responsibility |
|---|---|
| Tailwind Config File | Allows colors and config to be imported if using Tailwind v4 |
| ThemeProvider | Injects and scopes design tokens if not using Tailwind v4 |
| Component CSS Module | Handles structure & layout with Tailwind utilities |
| className Prop | Allows host-level overrides per instance |
| Build Pipeline | Compiles, scopes, and tree-shakes for minimal bundle size |
Benefits
- Zero component CSS collisions
- Fully theme-aware via CSS variables
- Reusable across multiple projects
- Works out of the box in any Vite or React setup
- Easy to extend at the component or theme level