React Component Library

Build a modern React component library with TypeScript and CSS Modules

React Component Library

Create a production-ready React component library with TypeScript, CSS Modules, and optimized bundling for both server and client components.

Overview

This example demonstrates:

  • React component library setup
  • TypeScript with React types
  • CSS Modules for scoped styling
  • Server and Client component separation
  • Tree-shakable exports
  • Storybook integration ready
  • Proper peer dependencies

Project Structure

react-ui-lib/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.module.css
│   │   │   └── index.ts
│   │   ├── Card/
│   │   │   ├── Card.tsx
│   │   │   ├── Card.module.css
│   │   │   └── index.ts
│   │   └── index.ts
│   ├── hooks/
│   │   ├── useLocalStorage.ts
│   │   └── index.ts
│   ├── utils/
│   │   ├── cn.ts
│   │   └── index.ts
│   ├── types/
│   │   └── index.ts
│   └── index.ts
├── package.json
├── packem.config.ts
└── tsconfig.json

Component Implementation

Button Component

src/components/Button/Button.tsx

'use client'

import React from 'react'
import { cn } from '../../utils/cn'
import styles from './Button.module.css'

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  loading?: boolean
  children: React.ReactNode
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({
    className,
    variant = 'primary',
    size = 'md',
    loading = false,
    disabled,
    children,
    ...props
  }, ref) => {
    return (
      <button
        className={cn(
          styles.button,
          styles[variant],
          styles[size],
          loading && styles.loading,
          className
        )}
        disabled={disabled || loading}
        ref={ref}
        {...props}
      >
        {loading && <span className={styles.spinner} />}
        <span className={loading ? styles.content : undefined}>
          {children}
        </span>
      </button>
    )
  }
)

Button.displayName = 'Button'

src/components/Button/Button.module.css

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  border-radius: 0.375rem;
  border: 1px solid transparent;
  font-weight: 500;
  transition: all 0.2s ease-in-out;
  cursor: pointer;
  position: relative;
  outline: none;
}

.button:focus-visible {
  ring: 2px solid #3b82f6;
  ring-offset: 2px;
}

.button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Variants */
.primary {
  background-color: #3b82f6;
  color: white;
}

.primary:hover:not(:disabled) {
  background-color: #2563eb;
}

.secondary {
  background-color: #6b7280;
  color: white;
}

.secondary:hover:not(:disabled) {
  background-color: #4b5563;
}

.outline {
  background-color: transparent;
  border-color: #d1d5db;
  color: #374151;
}

.outline:hover:not(:disabled) {
  background-color: #f9fafb;
  border-color: #9ca3af;
}

.ghost {
  background-color: transparent;
  color: #374151;
}

.ghost:hover:not(:disabled) {
  background-color: #f3f4f6;
}

/* Sizes */
.sm {
  height: 2rem;
  padding: 0 0.75rem;
  font-size: 0.875rem;
}

.md {
  height: 2.5rem;
  padding: 0 1rem;
  font-size: 0.875rem;
}

.lg {
  height: 3rem;
  padding: 0 1.5rem;
  font-size: 1rem;
}

/* Loading state */
.loading {
  cursor: not-allowed;
}

.content {
  opacity: 0;
}

.spinner {
  position: absolute;
  width: 1rem;
  height: 1rem;
  border: 2px solid transparent;
  border-top: 2px solid currentColor;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

src/components/Button/index.ts

export { Button } from './Button'
export type { ButtonProps } from './Button'

Card Component

src/components/Card/Card.tsx

import React from 'react'
import { cn } from '../../utils/cn'
import styles from './Card.module.css'

export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
  children: React.ReactNode
  padding?: 'none' | 'sm' | 'md' | 'lg'
  shadow?: 'none' | 'sm' | 'md' | 'lg'
}

export const Card = React.forwardRef<HTMLDivElement, CardProps>(
  ({ className, children, padding = 'md', shadow = 'md', ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(
          styles.card,
          styles[`padding-${padding}`],
          styles[`shadow-${shadow}`],
          className
        )}
        {...props}
      >
        {children}
      </div>
    )
  }
)

Card.displayName = 'Card'

export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
  children: React.ReactNode
}

export const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
  ({ className, children, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(styles.header, className)}
        {...props}
      >
        {children}
      </div>
    )
  }
)

CardHeader.displayName = 'CardHeader'

export interface CardContentProps extends React.HTMLAttributes<HTMLDivElement> {
  children: React.ReactNode
}

export const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>(
  ({ className, children, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(styles.content, className)}
        {...props}
      >
        {children}
      </div>
    )
  }
)

CardContent.displayName = 'CardContent'

src/components/Card/Card.module.css

.card {
  background-color: white;
  border-radius: 0.5rem;
  border: 1px solid #e5e7eb;
  overflow: hidden;
}

/* Padding variants */
.padding-none {
  padding: 0;
}

.padding-sm {
  padding: 0.75rem;
}

.padding-md {
  padding: 1.5rem;
}

.padding-lg {
  padding: 2rem;
}

/* Shadow variants */
.shadow-none {
  box-shadow: none;
}

.shadow-sm {
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}

.shadow-md {
  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}

.shadow-lg {
  box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}

.header {
  padding-bottom: 1rem;
  border-bottom: 1px solid #f3f4f6;
  margin-bottom: 1rem;
}

.content {
  /* Content styles */
}

src/components/Card/index.ts

export { Card, CardHeader, CardContent } from './Card'
export type { CardProps, CardHeaderProps, CardContentProps } from './Card'

Utility Functions

src/utils/cn.ts

/**
 * Utility function to combine class names
 */
export function cn(...classes: (string | undefined | null | false)[]): string {
  return classes.filter(Boolean).join(' ')
}

src/utils/index.ts

export { cn } from './cn'

Custom Hooks

src/hooks/useLocalStorage.ts

'use client'

import { useState, useEffect } from 'react'

export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
  // State to store our value
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === "undefined") {
      return initialValue
    }

    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error)
      return initialValue
    }
  })

  // Return a wrapped version of useState's setter function that persists the new value to localStorage
  const setValue = (value: T | ((val: T) => T)) => {
    try {
      // Allow value to be a function so we have the same API as useState
      const valueToStore = value instanceof Function ? value(storedValue) : value
      setStoredValue(valueToStore)

      // Save to localStorage
      if (typeof window !== "undefined") {
        window.localStorage.setItem(key, JSON.stringify(valueToStore))
      }
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error)
    }
  }

  return [storedValue, setValue]
}

src/hooks/index.ts

export { useLocalStorage } from './useLocalStorage'

Type Definitions

src/types/index.ts

export interface BaseProps {
  className?: string
  children?: React.ReactNode
}

export type Size = 'sm' | 'md' | 'lg'
export type Variant = 'primary' | 'secondary' | 'outline' | 'ghost'

Main Exports

src/components/index.ts

// Components
export * from './Button'
export * from './Card'

src/index.ts

// Components
export * from './components'

// Hooks
export * from './hooks'

// Utils
export * from './utils'

// Types
export * from './types'

Configuration

package.json

{
  "name": "@myorg/react-ui",
  "version": "1.0.0",
  "description": "Modern React component library",
  "type": "module",
  "files": [
    "dist"
  ],
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./components": {
      "types": "./dist/components/index.d.ts",
      "import": "./dist/components/index.js",
      "require": "./dist/components/index.cjs"
    },
    "./hooks": {
      "types": "./dist/hooks/index.d.ts",
      "import": "./dist/hooks/index.js",
      "require": "./dist/hooks/index.cjs"
    },
    "./utils": {
      "types": "./dist/utils/index.d.ts",
      "import": "./dist/utils/index.js",
      "require": "./dist/utils/index.cjs"
    }
  },
  "sideEffects": [
    "**/*.css"
  ],
  "scripts": {
    "build": "packem build",
    "dev": "packem build --watch",
    "clean": "rm -rf dist",
    "typecheck": "tsc --noEmit"
  },
  "keywords": [
    "react",
    "components",
    "ui",
    "typescript",
    "library"
  ],
  "peerDependencies": {
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },
  "devDependencies": {
    "@visulima/packem": "^2",
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0",
    "typescript": "^5.3.0"
  }
}

Packem Configuration

Quick Setup (Recommended):

Use the add command to automatically configure React:

npx packem add react

This will automatically set up the React preset and install all necessary dependencies.

packem.config.ts - Using String Preset

The simplest way to use the React preset is with a string:

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'

export default defineConfig({
  preset: 'react', // Use React preset for Babel configuration
  transformer,
  sourcemap: true,
  declaration: true,
})

packem.config.ts - Using Config Preset Function

For more control, use the createReactPreset function:

import { defineConfig } from '@visulima/packem/config'
import { createReactPreset } from '@visulima/packem/config/preset/react'
import transformer from '@visulima/packem/transformer/esbuild'

export default defineConfig({
  preset: createReactPreset({
    // Enable React Compiler for automatic optimizations
    compiler: false, // Set to true to enable React Compiler
    
    // Customize Babel presets (optional)
    presets: [
      // Additional presets can be added here
    ],
    
    // Customize Babel plugins (optional)
    plugins: [
      // Additional plugins can be added here
    ]
  }),
  transformer,
  sourcemap: true,
  declaration: true,
})

With React Compiler:

Enable React Compiler optimizations:

import { defineConfig } from '@visulima/packem/config'
import { createReactPreset } from '@visulima/packem/config/preset/react'
import transformer from '@visulima/packem/transformer/esbuild'

export default defineConfig({
  preset: createReactPreset({
    compiler: true // Enable React Compiler optimization
  }),
  transformer,
  sourcemap: true,
  declaration: true,
})

Alternative: Manual Babel Configuration

If you need complete control, you can configure Babel manually:

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'

export default defineConfig({
  transformer,
  sourcemap: true,
  declaration: true,
  externals: ['react', 'react-dom'],
  rollup: {
    babel: {
      presets: [
        ['@babel/preset-react', { runtime: 'automatic' }]
        // Note: TypeScript is handled by the transformer via parser plugins
        // Babel only needs to parse TypeScript syntax, not transform it
      ]
    },
  }
})

TypeScript Configuration

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "jsx": "react-jsx",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Usage Examples

Basic Usage

import { Button, Card, CardHeader, CardContent } from '@myorg/react-ui'

function App() {
  return (
    <Card>
      <CardHeader>
        <h2>Welcome</h2>
      </CardHeader>
      <CardContent>
        <p>This is a sample card with a button.</p>
        <Button variant="primary" onClick={() => alert('Clicked!')}>
          Click me
        </Button>
      </CardContent>
    </Card>
  )
}

With Hooks

import { Button, useLocalStorage } from '@myorg/react-ui'

function Counter() {
  const [count, setCount] = useLocalStorage('counter', 0)

  return (
    <div>
      <p>Count: {count}</p>
      <Button onClick={() => setCount(count + 1)}>
        Increment
      </Button>
    </div>
  )
}

Tree-shakable Imports

// Only import what you need
import { Button } from '@myorg/react-ui/components'
import { useLocalStorage } from '@myorg/react-ui/hooks'
import { cn } from '@myorg/react-ui/utils'

Building and Testing

Build the library:

npm run build

Output Structure

dist/
├── index.js              # ESM main entry
├── index.cjs             # CJS main entry
├── index.d.ts            # TypeScript declarations
├── components/
│   ├── index.js
│   ├── index.cjs
│   ├── index.d.ts
│   ├── Button/
│   │   ├── index.js
│   │   ├── index.cjs
│   │   ├── index.d.ts
│   │   └── Button.module.css
│   └── Card/
│       ├── index.js
│       ├── index.cjs
│       ├── index.d.ts
│       └── Card.module.css
├── hooks/
└── utils/

Key Features

CSS Modules Support

CSS is automatically processed and scoped:

// Styles are automatically imported and scoped
import styles from './Button.module.css'

// Generated class names: Button__button___a1b2c
<button className={styles.button}>Click me</button>

Server Components Ready

Components work in both server and client environments:

'use client' // Only when needed for interactivity

export const Button = () => {
  // This works in Next.js App Router
}

Type Safety

Full TypeScript support with proper component props:

<Button
  variant="primary"    // ✅ Valid variant
  size="lg"           // ✅ Valid size
  loading={true}      // ✅ Boolean prop
  onClick={handleClick} // ✅ Event handler
/>

External Dependencies

React and React DOM are properly externalized and won't be bundled.

Advanced Usage

Custom Styling

import { Button } from '@myorg/react-ui'

// Override styles with custom classes
<Button className="my-custom-button" variant="primary">
  Custom Button
</Button>

Compound Components

import { Card, CardHeader, CardContent } from '@myorg/react-ui'

<Card padding="lg" shadow="lg">
  <CardHeader>
    <h3>Card Title</h3>
  </CardHeader>
  <CardContent>
    <p>Card content goes here</p>
  </CardContent>
</Card>

This example provides a solid foundation for React component libraries with modern tooling, proper TypeScript support, and CSS Modules for styling.

Next Steps

  • Add Storybook for component documentation
  • Set up Jest and React Testing Library for testing
  • Add accessibility (a11y) features and testing
  • Implement dark mode support
  • Add more complex components (Modal, Dropdown, etc.)
  • Set up automated visual regression testing
Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues