Skip to content

A comprehensive Next.js application demonstrating two professional approaches to component styling: SCSS Modules vs Tailwind CSS + Class Variance Authority (CVA).

Notifications You must be signed in to change notification settings

Yevhenbk/cva-vs-scss

Repository files navigation

CVA vs SCSS: Button Component Styling Comparison

A comprehensive Next.js application demonstrating two professional approaches to component styling: SCSS Modules vs Tailwind CSS + Class Variance Authority (CVA).

Overview

This project provides an interactive comparison of button component implementations using two distinct architectural patterns. Both implementations feature identical visual designs with primary and secondary variants, demonstrating how different styling methodologies achieve the same user-facing results.

Components Included

  • [SCSS] Modules - Traditional CSS Modules approach with SCSS preprocessing
  • [CVA] Tailwind + CVA - Modern utility-first styling with type-safe variant composition

Project Architecture

src/
├── components/
│   ├── scss/
│   │   ├── ScssButton.tsx
│   │   └── ScssButton.module.css
│   ├── cva/
│   │   └── CvaButton.tsx
│   ├── Card.tsx
│   ├── ProConList.tsx
│   ├── Section.tsx
│   ├── DetailItem.tsx
│   └── ButtonShowcase.tsx
└── app/
    ├── page.tsx
    └── globals.css

Technical Comparison

CSS Modules Implementation

Bundle Impact: ~0.5KB (gzipped)
Performance: Static CSS generation with no runtime overhead

Advantages

  • Lightweight bundle contribution
  • CSS Modules scoping prevents conflicts
  • Native CSS variable support
  • Familiar to all CSS developers
  • Zero preprocessing overhead

Limitations

  • No compile-time property validation
  • Variant strings remain untyped
  • Runtime detection of invalid variants
  • No IDE autocompletion for variant names
  • Scales poorly with numerous variants

Tailwind + CVA Implementation

Bundle Impact: ~1.5KB (gzipped)
Performance: Minimal runtime class composition with negligible CPU overhead

Advantages

  • Full TypeScript type safety
  • Compile-time variant validation
  • Excellent IDE autocomplete and type hints
  • Scalable architecture for complex variant matrices
  • Industry standard for component libraries

Tradeoffs

  • Additional ~1KB gzipped bundle size
  • Requires Tailwind CSS framework
  • Steeper initial learning curve
  • Class strings can become lengthy
  • Build dependency on Tailwind CLI

Comparative Analysis

Criterion CSS Modules CVA + Tailwind
Type Safety None Full TypeScript support
Learning Curve Very Low Medium
Compile-Time Validation No Yes
IDE Autocompletion Limited Complete
Maintainability Good Excellent
Scalability Fair Excellent
Bundle Size Impact Minimal ~1KB gzipped
CSS Variable Integration Native Works seamlessly
Component Library Ready Possible Optimized

Bundle Size Breakdown

CSS Modules

  • CSS output only: ~0.5KB (gzipped)
  • No JavaScript runtime: 0KB
  • Total impact: ~0.5KB

CVA + Tailwind

  • CVA library: ~0.8KB
  • Tailwind utilities: ~0.7KB
  • Total impact: ~1.5KB

Difference

The 1KB additional footprint represents approximately 0.3% of a typical SPA bundle, with negligible real-world performance impact.


Performance Characteristics

CSS Modules

  • Style generation: Build-time
  • Runtime execution: None
  • Class calculation: Not applicable
  • Ideal for: Static scoped CSS delivery

CVA + Tailwind

  • Style generation: Build-time + minimal runtime
  • Runtime execution: Class name composition
  • CPU overhead: Negligible per render
  • Ideal for: Dynamic variant selection

Both approaches are production-ready with equivalent real-world performance characteristics.


Implementation Examples

CSS Modules Button Component

import React from 'react';
import styles from './ScssButton.module.css';

interface ScssButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary';
}

export function ScssButton({
  variant = 'primary',
  children,
  ...props
}: ScssButtonProps) {
  return (
    <button className={`${styles.button} ${styles[variant]}`} {...props}>
      {children}
    </button>
  );
}

CSS Module:

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 0.5rem;
  cursor: pointer;
  transition: all 0.2s ease-in-out;
}

.primary {
  background-color: var(--primary-bg);
  color: var(--primary-text);
  padding: 0.75rem 1.5rem;
  box-shadow: var(--shadow-sm);
}

.primary:hover:not(:disabled) {
  background-color: var(--primary-bg-hover);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

.secondary {
  background-color: var(--secondary-bg);
  color: var(--secondary-text);
  border: 2px solid var(--secondary-border);
  padding: calc(0.75rem - 2px) calc(1.5rem - 2px);
}

.secondary:hover:not(:disabled) {
  background-color: var(--secondary-bg-hover);
  box-shadow: var(--shadow-sm);
  transform: translateY(-2px);
}

CVA Button Component

import { cva, type VariantProps } from 'class-variance-authority';
import React from 'react';

const buttonVariants = cva(
  'inline-flex items-center justify-center font-semibold rounded-lg border-none cursor-pointer transition-all duration-200 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed',
  {
    variants: {
      variant: {
        primary: [
          'px-6 py-3 shadow-sm',
          'bg-primary-bg text-primary-text',
          'hover:bg-primary-bg-hover hover:shadow-md hover:-translate-y-0.5',
          'active:translate-y-0',
        ].join(' '),
        secondary: [
          'px-6 py-3',
          'bg-secondary-bg text-secondary-text border-2 border-secondary-border',
          'hover:bg-secondary-bg-hover hover:shadow-sm hover:-translate-y-0.5',
          'active:translate-y-0',
        ].join(' '),
      },
    },
    defaultVariants: {
      variant: 'primary',
    },
  }
);

interface CvaButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  children: React.ReactNode;
}

export function CvaButton({
  variant,
  children,
  className = '',
  ...props
}: CvaButtonProps) {
  return (
    <button className={`${buttonVariants({ variant })} ${className}`} {...props}>
      {children}
    </button>
  );
}

Recommendation

For Component Libraries

Use Tailwind + CVA for optimal maintainability and developer experience. Type-safe variants eliminate entire categories of bugs and provide superior IDE support. The 1KB additional bundle size is negligible for production applications.

For Large-Scale Applications

The 1KB difference is typically 0.2-0.3% of total bundle size and has zero measurable performance impact. Developer productivity gains substantially exceed any theoretical bundle cost.

Hybrid Approach

Both methodologies coexist effectively within the same codebase:

Component Library
├── CVA + Tailwind for UI components (buttons, inputs, cards, modals)
├── SCSS Modules for specialized styling (complex layouts, animations)
└── Tailwind Utilities for one-off adjustments

This separation provides maximum flexibility while maintaining clean architecture.


Getting Started

Installation

npm install
npm run dev

Visit http://localhost:3000 to view the interactive comparison.

Dependencies

  • Next.js 16+ (App Router)
  • React 19+
  • Tailwind CSS 4+
  • Class Variance Authority (CVA)
  • TypeScript 5+

Conclusion

CSS Modules remains an excellent choice for projects with established CSS architecture and teams prioritizing minimal bundle size. It provides proven, familiar patterns with straightforward mental models and zero preprocessing overhead.

Tailwind + CVA represents the modern standard for component-driven development, offering superior type safety, maintainability, and scalability. The minimal bundle overhead is justified by dramatic improvements in developer experience and code reliability.

Choose the approach aligned with your team's expertise, project requirements, and architectural philosophy. Both are production-ready solutions.


Last Updated: December 2025
Next.js Version: Latest (App Router)
Styling Stack: Tailwind CSS 4 + CSS Modules + CVA

About

A comprehensive Next.js application demonstrating two professional approaches to component styling: SCSS Modules vs Tailwind CSS + Class Variance Authority (CVA).

Topics

Resources

Stars

Watchers

Forks