Styling in React

CSS modules, styled-components, and Tailwind CSS

Styling Options in React

React doesn't dictate how you style your components. You have many options: plain CSS, CSS modules, CSS-in-JS libraries, and utility-first frameworks like Tailwind. Each has its trade-offs.

Inline Styles

Pass a JavaScript object to the style prop:

function InlineStyles() {
  const buttonStyle = {
    backgroundColor: 'blue',
    color: 'white',
    padding: '10px 20px',
    border: 'none',
    borderRadius: '4px',
    cursor: 'pointer'
  };

  // Dynamic styles
  const [isActive, setIsActive] = useState(false);

  return (
    <div>
      <button style={buttonStyle}>Static Style</button>
      
      <button 
        style={{
          ...buttonStyle,
          backgroundColor: isActive ? 'green' : 'blue'
        }}
        onClick={() => setIsActive(!isActive)}
      >
        Dynamic Style
      </button>
    </div>
  );
}

⚠️ Inline Style Limitations

  • • No pseudo-classes (:hover, :focus)
  • • No media queries
  • • No keyframe animations
  • • camelCase properties (backgroundColor, not background-color)

Plain CSS

Import CSS files directly into your components:

/* Button.css */
.button {
  background-color: blue;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
}

.button:hover {
  background-color: darkblue;
}

.button.active {
  background-color: green;
}
// Button.jsx
import './Button.css';

function Button({ isActive, children }) {
  return (
    <button className={`button ${isActive ? 'active' : ''}`}>
      {children}
    </button>
  );
}

CSS Modules

Locally scoped CSS that avoids naming conflicts:

/* Button.module.css */
.button {
  background-color: blue;
  color: white;
  padding: 10px 20px;
}

.button:hover {
  background-color: darkblue;
}

.primary {
  background-color: blue;
}

.secondary {
  background-color: gray;
}
// Button.jsx
import styles from './Button.module.css';

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

// Classes are transformed to unique names:
// .button_abc123 .primary_xyz789

Tailwind CSS

Utility-first CSS framework—extremely popular with React:

// No separate CSS file needed!
function Button({ variant = 'primary', children }) {
  const baseClasses = 'px-4 py-2 rounded font-semibold transition-colors';
  
  const variants = {
    primary: 'bg-blue-500 text-white hover:bg-blue-600',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
    danger: 'bg-red-500 text-white hover:bg-red-600'
  };

  return (
    <button className={`${baseClasses} ${variants[variant]}`}>
      {children}
    </button>
  );
}

// Card component
function Card({ title, children }) {
  return (
    <div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
      <h3 className="text-xl font-bold mb-4 text-gray-900">{title}</h3>
      <div className="text-gray-600">{children}</div>
    </div>
  );
}

// Responsive design
function ResponsiveGrid() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      <Card title="Card 1">Content</Card>
      <Card title="Card 2">Content</Card>
      <Card title="Card 3">Content</Card>
    </div>
  );
}

clsx / classnames

Utility for conditionally joining class names:

import clsx from 'clsx';
// or: import classNames from 'classnames';

function Button({ variant, size, disabled, className }) {
  return (
    <button
      className={clsx(
        // Base classes (always applied)
        'font-semibold rounded transition-colors',
        // Variant classes
        {
          'bg-blue-500 text-white': variant === 'primary',
          'bg-gray-200 text-gray-800': variant === 'secondary',
          'bg-red-500 text-white': variant === 'danger',
        },
        // Size classes
        {
          'px-2 py-1 text-sm': size === 'small',
          'px-4 py-2': size === 'medium',
          'px-6 py-3 text-lg': size === 'large',
        },
        // State classes
        disabled && 'opacity-50 cursor-not-allowed',
        // Custom classes from props
        className
      )}
      disabled={disabled}
    >
      Click me
    </button>
  );
}

Styled Components

CSS-in-JS library for component-level styles:

import styled from 'styled-components';

// Create a styled button
const Button = styled.button`
  background-color: ${props => props.primary ? 'blue' : 'gray'};
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: ${props => props.primary ? 'darkblue' : 'darkgray'};
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`;

// Extending styles
const PrimaryButton = styled(Button)`
  background-color: blue;
  font-weight: bold;
`;

// Using props for dynamic styles
const Card = styled.div`
  background: white;
  border-radius: 8px;
  padding: ${props => props.padding || '16px'};
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
`;

function App() {
  return (
    <Card padding="24px">
      <Button>Normal</Button>
      <Button primary>Primary</Button>
      <PrimaryButton>Extended</PrimaryButton>
    </Card>
  );
}

Comparison Table

Approach Pros Cons
Inline Styles Simple, dynamic, no files No pseudo-classes, verbose
Plain CSS Full CSS features, familiar Global scope, naming conflicts
CSS Modules Scoped, full CSS features Extra files, build setup
Tailwind CSS Fast, consistent, small bundle Learning curve, long classes
Styled Components Scoped, dynamic, full CSS Runtime cost, extra dependency

🎯 Styling Best Practices

  • ✓ Choose one primary approach and be consistent
  • ✓ Use CSS Modules or Tailwind for most projects
  • ✓ Use clsx/classnames for conditional classes
  • ✓ Keep styles close to components
  • ✓ Use CSS variables for theming
  • ✓ Avoid inline styles except for truly dynamic values
  • ✓ Consider design system libraries (Chakra UI, Radix, shadcn/ui)