Equality

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 className prop

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:

  1. TypeScript builds the component logic.
  2. PostCSS + Tailwind transform .module.css files.
  3. CSS Modules scope the output so class names never collide.
  4. Theme tokens are exported as a Tailwind configuration file and also embedded and exposed through a <ThemeProvider /> to allow for integration in various frameworks.
  5. Output goes into /dist as 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

LayerResponsibility
Tailwind Config FileAllows colors and config to be imported if using Tailwind v4
ThemeProviderInjects and scopes design tokens if not using Tailwind v4
Component CSS ModuleHandles structure & layout with Tailwind utilities
className PropAllows host-level overrides per instance
Build PipelineCompiles, 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