v0.1.6·Development
Customization
Understanding the CSS variable system, CSS Modules pattern, and how to customize component styling.
Published: 12/14/2025

Styling & CSS Architecture

UI Lab uses a sophisticated styling system combining CSS Modules for component encapsulation, semantic CSS variables for theming, and Tailwind CSS for layout utilities. This guide explains how the styling system works and how to customize it.


Architecture Overview

The styling system is built on three foundations:

  1. CSS Modules – Scoped component styles to prevent conflicts
  2. Semantic CSS Variables – Consistent theming across all components
  3. Tailwind CSS v4 – Utility classes for layout and responsive design
Component Styles
    ↓
CSS Modules (scoped)
    ↓
CSS Variables (semantic)
    ↓
Tailwind CSS v4 (@apply)
    ↓
PostCSS Pipeline
    ↓
Compiled CSS Output

CSS Module Pattern

Each component uses CSS Modules for scoped, encapsulated styling:

File Structure

button/
├── button.tsx              # React component
├── button.module.css       # Component styles
├── button.module.css.d.ts  # Type definitions
└── index.ts               # Exports

CSS Modules Basics

button.module.css:

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--button-padding);
  border-radius: var(--radius-md);
  font-weight: 600;
  cursor: pointer;
  transition: all var(--transition-fast);
}

.button[data-variant="primary"] {
  background-color: var(--color-primary);
  color: white;
}

.button[data-variant="secondary"] {
  background-color: var(--color-background-surface);
  border: 1px solid var(--color-background-border);
}

.button[data-size="sm"] {
  padding: var(--spacing-1) var(--spacing-2);
  font-size: var(--text-sm);
}

.button[data-size="lg"] {
  padding: var(--spacing-3) var(--spacing-4);
  font-size: var(--text-lg);
}

button.tsx:

import styles from './button.module.css';

export function Button({ variant = 'primary', size = 'md', ...props }) {
  return (
    <button
      className={styles.button}
      data-variant={variant}
      data-size={size}
      {...props}
    />
  );
}

Benefits

  • Scoped Classes – Class names are local to the component
  • No Naming Conflicts – Component can't interfere with other styles
  • Type Safety – TypeScript knows available class names
  • Easy Overrides – CSS variables make customization straightforward
  • Tree Shakeable – Unused component styles are removed

CSS Variable System

All components use semantic CSS variables for consistent theming. Variables are organized by category:

Typography Variables

Text Sizes:

--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--text-5xl: 3rem;

Font Families:

--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: "Fira Code", "Courier New", monospace;

Font Weights:

--font-normal: 400;
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;

Line Heights:

--line-height-tight: 1.2;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;

Color Variables

Colors use OKLch color space (perceptually uniform) with numeric scale 50–950:

Background Colors:

--background-50:   oklch(99.2% 0.001 240);  /* Lightest */
--background-100:  oklch(97.5% 0.002 240);
--background-200:  oklch(95.0% 0.004 240);
/* ... 300-900 continue the scale ... */
--background-950:  oklch(11.8% 0.018 240);  /* Darkest */

Semantic Aliases:

--background-surface:    var(--background-100);   /* Base surface */
--background-elevated:   var(--background-50);    /* Floating elements */
--background-contrast:   var(--background-950);   /* Maximum contrast */
--background-border:     var(--background-200 / 0.12);

Foreground/Text & Semantic Colors:

CategoryVariablesPurpose
Text--foreground-50 to --foreground-950Text color scale
Text Aliases--foreground-primary, -secondary, -mutedSemantic text roles
Brand--color-primary, --color-secondaryPrimary/secondary actions
Status--color-success, -warning, -danger, -infoFeedback states

Spacing Variables

Responsive spacing scale:

--spacing-1:  0.25rem;   /* 4px */
--spacing-2:  0.5rem;    /* 8px */
--spacing-3:  0.75rem;   /* 12px */
--spacing-4:  1rem;      /* 16px */
--spacing-6:  1.5rem;    /* 24px */
--spacing-8:  2rem;      /* 32px */
--spacing-12: 3rem;      /* 48px */
--spacing-16: 4rem;      /* 64px */

Layout Variables

Border Radius:

--radius-xs:   0.125rem;  /* Extra small */
--radius-sm:   0.25rem;   /* Small */
--radius-md:   0.375rem;  /* Medium */
--radius-lg:   0.5rem;    /* Large */
--radius-xl:   0.75rem;   /* Extra large */
--radius-full: 9999px;    /* Circle */

Borders:

--border-width-1: 1px;
--border-width-2: 2px;
--border-width-4: 4px;

Shadows:

--shadow-xs:     0 1px 2px rgb(0 0 0 / 0.05);
--shadow-sm:     0 1px 3px rgb(0 0 0 / 0.1);
--shadow-card:   0 4px 16px rgb(0 0 0 / 0.1);
--shadow-modal:  0 12px 48px rgb(0 0 0 / 0.24);

Transitions:

--transition-fast:      120ms cubic-bezier(0.2, 0, 0.4, 1);
--transition-normal:    180ms cubic-bezier(0.3, 0, 0.3, 1);
--transition-smooth:    250ms cubic-bezier(0.4, 0, 0.2, 1);

Using CSS Variables

In component styles:

.button {
  padding: var(--spacing-2) var(--spacing-4);
  font-size: var(--text-sm);
  border-radius: var(--radius-md);
  background-color: var(--color-primary);
  transition: all var(--transition-fast);
}

Data Attributes for Styling

Components use data attributes to represent state and variants. These allow CSS to style based on component props:

Available Data Attributes

Variants & Sizes:

<button data-variant="primary" data-size="lg" />

Styled with:

button[data-variant="primary"] {
  background-color: var(--color-primary);
}

button[data-size="lg"] {
  padding: var(--spacing-3) var(--spacing-4);
}

States:

<button data-disabled data-focused data-hovered />

Styled with:

button[data-disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}

button[data-focused] {
  outline: 2px solid var(--color-primary);
}

button[data-hovered] {
  background-color: var(--color-primary-600);
}

Interactive States:

button[data-pressed] {
  transform: scale(0.98);
}

button[data-focus-visible] {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}

Complete State Example

<Input
  data-disabled={disabled}
  data-focused={isFocused}
  data-error={hasError}
  data-size="lg"
/>
input[data-error] {
  border-color: var(--color-danger);
}

input[data-error]:focus {
  box-shadow: 0 0 0 3px var(--color-danger / 0.1);
}

input[data-size="lg"] {
  padding: var(--spacing-2) var(--spacing-3);
  font-size: var(--text-base);
}

Tailwind CSS Integration

UI Lab uses Tailwind CSS v4 for layout utilities and responsive design. The @theme directive defines all design tokens:

@theme Directive

Root CSS file uses @theme to define all variables:

@theme {
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  /* ... all variables ... */

  --color-primary: oklch(68% 0.22 245);
  --color-danger: oklch(65% 0.28 25);
  /* ... all colors ... */
}

@apply Directive in Components

CSS Modules can use @apply to compose Tailwind utilities:

.button {
  @apply inline-flex items-center justify-center font-medium transition-all;
}

.button[data-size="sm"] {
  @apply px-2 py-1 text-sm;
}

.button[data-size="lg"] {
  @apply px-6 py-3 text-base;
}

.button:hover {
  @apply shadow-md;
}

Why This Approach

  • Zero Runtime Overhead – No CSS-in-JS processing
  • Composable – Tailwind utilities + CSS variables work together
  • Customizable – Change any variable in @theme for full customization
  • Responsive – Use Tailwind responsive prefixes (sm:, md:, lg:)
  • Dark Mode – Variables automatically work with light/dark themes

Customizing Component Styles

Override CSS variables globally to change all components at once:

:root {
  --color-primary: oklch(50% 0.25 150);
  --spacing-4: 1.25rem;
  --radius-md: 0.5rem;
}

Method 2: CSS Modules Override

Create wrapper components with custom styles:

/* MyButton.module.css */
.customButton {
  @apply font-bold text-lg;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
import styles from './MyButton.module.css';
import { Button } from 'ui-lab-components';

export function MyButton(props) {
  return <Button {...props} className={styles.customButton} />;
}

Method 3: Tailwind Utilities

Combine with Tailwind classes for layout:

<div className="flex gap-4">
  <Button className="flex-1">Primary</Button>
  <Button variant="secondary" className="flex-1">Secondary</Button>
</div>

Theme Customization

Preset Themes

UI Lab provides preset themes:

vitesse-light – Light theme with cool colors

@theme {
  --background-50: oklch(99.2% 0.001 240);
  --color-primary: oklch(68% 0.22 245);
}

vitesse-dark – Dark theme with high contrast

@theme {
  --background-950: oklch(11.8% 0.018 240);
  --color-primary: oklch(68% 0.22 245);
}

Creating Custom Themes

Generate a custom theme file during CLI setup:

npx ui-lab init --preset custom

Edit the generated theme:

/* themes/custom.css */
@theme {
  /* Brand colors */
  --color-primary: oklch(60% 0.25 15);     /* Your orange */
  --color-secondary: oklch(50% 0.20 280);  /* Your purple */

  /* Background scale */
  --background-50: oklch(98% 0.001 20);
  --background-100: oklch(96% 0.002 20);
  /* ... define full scale ... */
  --background-950: oklch(12% 0.020 20);

  /* Typography */
  --text-base: 1.125rem;  /* Larger base */

  /* Spacing */
  --spacing-4: 1.25rem;   /* More spacious */
}

Import in your app:

// app.tsx or app.css
import './themes/custom.css';

Dark Mode Support

CSS variables automatically support light/dark mode:

:root {
  --background-50: oklch(99.2% 0.001 240);   /* Light mode */
  --foreground-950: oklch(11.8% 0.018 240);
}

@media (prefers-color-scheme: dark) {
  :root {
    --background-50: oklch(11.8% 0.018 240);  /* Dark mode */
    --foreground-950: oklch(99.2% 0.001 240);
  }
}

Or manually switch:

document.documentElement.style.setProperty('--background-50', 'oklch(11.8% 0.018 240)');

Advanced Styling Techniques

Responsive Design with CSS Variables

Use CSS variables with responsive design:

:root {
  --spacing-4: 1rem;      /* Mobile */
  --text-base: 0.875rem;  /* Mobile text */
}

@media (min-width: 768px) {
  :root {
    --spacing-4: 1.5rem;  /* Tablet */
    --text-base: 1rem;    /* Tablet text */
  }
}

All components using these variables automatically respond.

Color Scales for Consistency

OKLch color space maintains perceptual uniformity:

/* Light mode */
:root {
  --color-primary-50:  oklch(95% 0.05 245);  /* Lightest */
  --color-primary-100: oklch(90% 0.10 245);
  --color-primary-500: oklch(68% 0.22 245);  /* Primary */
  --color-primary-900: oklch(25% 0.18 245);  /* Darkest */
}

/* Dark mode - automatically inverted */
@media (prefers-color-scheme: dark) {
  :root {
    --color-primary-50:  oklch(25% 0.18 245);   /* Now dark */
    --color-primary-900: oklch(95% 0.05 245);   /* Now light */
  }
}

Animations with Transitions

Use transition variables for consistent animation timing:

.button {
  transition: all var(--transition-fast);
}

.button:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-card);
}

.modal {
  transition: opacity var(--transition-normal);
}

CSS Variable Reference

Quick reference for all available variables:

CategoryExamplesUsage
Typography--text-sm, --text-base, --text-3xlFont sizes
Colors--color-primary, --background-500All colors
Spacing--spacing-2, --spacing-4, --spacing-8Padding/margin
Radius--radius-sm, --radius-md, --radius-fullBorder radius
Shadows--shadow-sm, --shadow-cardDrop shadows
Transitions--transition-fast, --transition-smoothAnimation timing

Best Practices

1. Use CSS Variables for Theming

Do this:

button {
  background-color: var(--color-primary);
}

Don't do this:

button {
  background-color: #0066ff;  /* Hardcoded color */
}

2. Scope Styles with CSS Modules

Do this:

/* button.module.css */
.button {
  padding: var(--spacing-2) var(--spacing-4);
}

Don't do this:

/* global.css */
button {
  padding: 0.5rem 1rem;  /* Can conflict */
}

3. Use Data Attributes for Variants

Do this:

<button data-variant="primary" data-size="lg" />

Don't do this:

<button className="button-primary button-lg" />

4. Avoid CSS-in-JS

Do this:

/* styles.module.css */
.button {
  background-color: var(--color-primary);
}

Don't do this:

const buttonStyle = {
  backgroundColor: '#0066ff',  /* Runtime overhead */
};

5. Leverage @apply for Composition

Do this:

.button {
  @apply inline-flex items-center justify-center;
}

Don't do this:

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

Summary

The UI Lab styling system provides:

  • CSS Modules – Scoped, encapsulated component styles
  • Semantic CSS Variables – Consistent, customizable theming
  • Tailwind CSS v4 – Modern utility-first layout system
  • Dark Mode Support – Automatic theme switching
  • Data Attributes – Clean prop-to-style mapping
  • Zero Runtime – Pure CSS, no processing overhead

This combination creates a powerful, maintainable, and performant styling system that scales from small projects to large applications.

© 2025 UI Lab • Built for humans and machines