Telerik blogs

The React landscape has many styling solutions to choose from. The best styling solution is one that helps you ship great user experiences while keeping your codebase maintainable and your team productive.

Styling React components has evolved over the years. From inline styles to CSS-in-JS solutions, and from component libraries to utility-first frameworks, developers now have plenty of options for creating visually appealing and maintainable user interfaces.

In this article, we’ll explore some of the more popular and effective methods for styling React components today. Whether you’re building a small personal project or architecting a large-scale enterprise app, understanding the differences between these styling approaches will help you make more informed decisions when architecting your app’s user interface.

styled-components remains one of the most popular CSS-in-JS solutions. CSS-in-JS allows us to write actual CSS code within JavaScript files using tagged template literals.

The concept behind styled-components is creating React components with styles attached directly to them. Instead of writing separate CSS files and applying class names, we define styled components that encapsulate both the element type and its styles.

import styled from 'styled-components';

const Button = styled.button`
  background-color: ${props => props.primary ? '#007bff' : '#6c757d'};
  color: white;
  padding: 12px 24px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: background-color 0.2s ease;

  &:hover {
    background-color: ${props => props.primary ? '#0056b3' : '#5a6268'};
  }

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

// Usage
function App() {
  return (
    <div>
      <Button primary>Primary Button</Button>
      <Button>Secondary Button</Button>
    </div>
  );
}

In the above example, we’re creating a Button component using styled.button and the template literal contains regular CSS but with some additions. The ${props => ...} syntax allows us to dynamically change styles based on props passed to the component:

  • When we use <Button primary>, the button receives a blue background.
  • Without the primary prop, the button gets a gray background.
  • The &:hover and &:disabled selectors work just like in regular CSS by applying styles for different states.

Some of the main benefits of styled-components include automatic critical CSS extraction and no class name collisions. However, it does add to the app’s JavaScript bundle size and requires a runtime.

CSS Modules | Locally Scoped CSS

CSS Modules offer a different approach by keeping CSS in separate files while automatically generating unique class names to avoid global namespace pollution. This method has become popular among folks who prefer the separation of concerns while still wanting scoped styles.

The key to CSS Modules is that they need to be configured in a build tool like webpack, Vite, or Next.js. Here’s an example of creating a CSS Module file:

/* Button.module.css (or Button.css, depending on your build config) */
.button {
  padding: 12px 24px;
  border: none;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.primary {
  background-color: #007bff;
  color: white;
}

.primary:hover {
  background-color: #0056b3;
}

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

.secondary:hover {
  background-color: #5a6268;
}

.disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

Here’s an example of how we can use these styles in a React Button component:

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

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

export default Button;

When we import a CSS Module (e.g., import styles from './Button.module.css'), we get an object where each class name from our CSS file becomes a property. The actual class names in the DOM will be transformed into something unique, like Button_button__2Je3C, to ensure there are no global conflicts.

The great thing about CSS Modules is that they compile to a low-level interchange format called ICSS (Interoperable CSS), which handles the local scoping magic while keeping our CSS looking like normal CSS.

Tailwind CSS | The Utility-First Revolution

Tailwind CSS has changed how a large number of developers approach styling by providing utility classes we can compose directly in JSX, eliminating the need to write custom CSS in most cases.

Each class in Tailwind represents a specific CSS property and value. Here’s a simple example of a ProductCard component that references many different Tailwind utility classes:

function ProductCard({ product }) {
  return (
    <div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
      <img 
        src={product.imageUrl} 
        alt={product.name}
        className="w-full h-48 object-cover"
      />
      <div className="p-6">
        <h3 className="text-xl font-semibold text-gray-800 mb-2">
          {product.name}
        </h3>
        <p className="text-gray-600 mb-4 line-clamp-2">
          {product.description}
        </p>
        <div className="flex justify-between items-center">
          <span className="text-2xl font-bold text-indigo-600">
            ${product.price}
          </span>
          <button className="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md transition-colors duration-200">
            Add to Cart
          </button>
        </div>
      </div>
    </div>
  );
}

Let’s decode what some of these utility classes do:

While Tailwind’s utility classes are powerful, we’ll often want to create reusable component patterns. Here’s an example of building a more flexible button component that allows for customization:

import { forwardRef } from 'react';
import clsx from 'clsx';

const Button = forwardRef(({ 
  variant = 'primary', 
  size = 'medium', 
  className,
  children,
  ...props 
}, ref) => {
  const baseStyles = 'inline-flex items-center justify-center font-medium rounded-md transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2';
  
  const variants = {
    primary: 'bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-500',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
    danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
    ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500'
  };
  
  const sizes = {
    small: 'px-3 py-1.5 text-sm',
    medium: 'px-4 py-2 text-base',
    large: 'px-6 py-3 text-lg'
  };
  
  return (
    <button
      ref={ref}
      className={clsx(
        baseStyles,
        variants[variant],
        sizes[size],
        className
      )}
      {...props}
    >
      {children}
    </button>
  );
});

Button.displayName = 'Button';

export default Button;

In the above component, we define baseStyles that apply to all button variants, while the variants and sizes objects map prop values to specific utility class combinations. The clsx utility (a popular className concatenation library) combines these classes intelligently, and the optional className prop allows consumers to add additional styles when needed.

Tailwind’s utility-first approach offers rapid development and consistent design implementation. However, it can lead to verbose HTML and has a learning curve for developers accustomed to traditional CSS approaches.

For a deeper dive into Tailwind and how it can be used with enterprise component libraries like Progress KendoReact, be sure to check out this previous article: A Primer on Tailwind CSS: Pros, Cons and Real-World Use Cases.

Setting Up a Theme

No matter which styling method you choose, implementing a consistent theme across your application is often a core aspect of maintaining design coherence. One common method of implementing an application-wide theme is to use CSS variables, which provide a performant way to implement theming without causing React rerenders. We can set up a CSS variable-based theme system by first defining theme-related CSS variables:

/* global.css */
:root {
  /* Light theme (default) */
  --color-primary: #007bff;
  --color-secondary: #6c757d;
  --color-background: #ffffff;
  --color-surface: #f8f9fa;
  --color-text: #212529;
  --color-text-secondary: #6c757d;
  --color-border: #dee2e6;
  
  --shadow-small: 0 1px 3px rgba(0, 0, 0, 0.12);
  --shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.1);
  --shadow-large: 0 10px 15px rgba(0, 0, 0, 0.1);
  
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
}

[data-theme="dark"] {
  --color-primary: #0d6efd;
  --color-secondary: #6c757d;
  --color-background: #212529;
  --color-surface: #343a40;
  --color-text: #f8f9fa;
  --color-text-secondary: #adb5bd;
  --color-border: #495057;
  
  --shadow-small: 0 1px 3px rgba(0, 0, 0, 0.3);
  --shadow-medium: 0 4px 6px rgba(0, 0, 0, 0.3);
  --shadow-large: 0 10px 15px rgba(0, 0, 0, 0.3);
}

/* Smooth theme transitions */
* {
  transition: background-color 0.3s ease, color 0.3s ease;
}

These CSS variables are defined at the root level and automatically switch when the data-theme attribute changes on the document element. The transition rule ensures smooth color changes when switching themes. Now we can use these CSS variables in our components regardless of our styling approach:

// Using CSS variables with CSS Modules
/* Card.module.css */
.card {
  background-color: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: var(--spacing-lg);
  box-shadow: var(--shadow-medium);
}

// Or with styled-components
const Card = styled.div`
  background-color: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: var(--spacing-lg);
  box-shadow: var(--shadow-medium);
`;

Another approach to setting up a consistent theme across a React application is to leverage React’s Context API.

For more details on the different ways to introduce a UI theme in a React project, be sure to check out this previous article: How to Introduce a UI Theme in Your React App.

Component Libraries

While building custom components gives us complete control over styling and behavior, leveraging established component libraries can significantly accelerate development.

Component libraries are particularly valuable when we need production-ready components with built-in accessibility, comprehensive documentation, consistent design language across components, and complex functionality like data grids or date pickers. They’re especially beneficial for enterprise applications where time-to-market and reliability are crucial.

KendoReact: Enterprise-Grade Components

Progress KendoReact stands out as a comprehensive UI component library that offers over 120+ components designed for professional applications. What makes the KendoReact library particularly appealing is its attention to enterprise needs and its flexible approach to styling.

Here’s a simple example of how we can implement a more complex data management interface pretty easily with the use of certain KendoReact components:

import { Grid, GridColumn } from '@progress/kendo-react-grid';
import { Button } from '@progress/kendo-react-buttons';
import { DatePicker } from '@progress/kendo-react-dateinputs';
import '@progress/kendo-theme-bootstrap/dist/all.css';

function OrderManagement() {
  const [orders, setOrders] = useState([
    {
      id: 1,
      customerName: 'John Doe',
      orderDate: new Date(2025, 0, 15),
      total: 156.99,
      status: 'Shipped'
    },
    {
      id: 2,
      customerName: 'Jane Smith',
      orderDate: new Date(2025, 0, 14),
      total: 234.50,
      status: 'Processing'
    }
  ]);

  const [selectedDate, setSelectedDate] = useState(new Date());

  return (
    <div className="order-management">
      <div className="filters">
        <DatePicker
          value={selectedDate}
          onChange={(e) => setSelectedDate(e.value)}
          format="dd/MM/yyyy"
          placeholder="Filter by date"
        />
        <Button themeColor="primary" icon="filter">
          Apply Filters
        </Button>
      </div>

      <Grid
        data={orders}
        style={{ height: '400px' }}
        sortable={true}
        pageable={true}
      >
        <GridColumn field="id" title="Order ID" width="100px" />
        <GridColumn field="customerName" title="Customer" />
        <GridColumn field="orderDate" title="Date" format="{0:d}" />
        <GridColumn field="total" title="Total" format="{0:c}" />
        <GridColumn field="status" title="Status" />
      </Grid>
    </div>
  );
}

This example showcases several KendoReact components working together. The DatePicker provides a fully-featured date selection interface with format validation. The Button component includes built-in theming support through the themeColor prop and can display icons. The Grid component is particularly powerful as it automatically handles sorting (when sortable={true}), pagination (when pageable={true}), and data formatting through the format prop on columns.

The beauty of the KendoReact suite lies in its flexibility. We can use it with its built-in themes for rapid development or customize it to match our design system perfectly. With the KendoReact Free library offering 50+ components at no cost, it’s become an attractive option for projects of all sizes.

Best Practices for Styling React Components

As we’ve explored various styling approaches in this article, there are some general best practices to keep in mind.

Maintain Consistency

Whether using styled-components, CSS Modules or Tailwind, try to maintain consistency throughout your project. Mixing multiple approaches could lead to confusion and maintenance challenges.

Design Tokens

Define your design system values (colors, spacing, typography) in a centralized location (e.g., CSS Variables, design tokens file, etc.). This creates a single source of truth for your design system:

// design-tokens.js
export const tokens = {
  colors: {
    brand: {
      primary: '#007bff',
      primaryDark: '#0056b3',
      secondary: '#6c757d'
    },
    semantic: {
      error: '#dc3545',
      warning: '#ffc107',
      success: '#28a745',
      info: '#17a2b8'
    }
  },
  typography: {
    fontFamily: {
      sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
      mono: 'SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace'
    },
    fontSize: {
      xs: '0.75rem',
      sm: '0.875rem',
      base: '1rem',
      lg: '1.125rem',
      xl: '1.25rem'
    }
  },
  spacing: {
    0: '0',
    1: '0.25rem',
    2: '0.5rem',
    3: '0.75rem',
    4: '1rem',
    5: '1.25rem',
    6: '1.5rem',
    8: '2rem'
  }
};

Component Composition

Build complex components by composing simpler ones. This helps promote reusability and maintainability:

// Base components
const Box = styled.div`
  padding: ${props => props.padding || '0'};
  margin: ${props => props.margin || '0'};
  background: ${props => props.background || 'transparent'};
`;

const Flex = styled(Box)`
  display: flex;
  flex-direction: ${props => props.direction || 'row'};
  align-items: ${props => props.align || 'stretch'};
  justify-content: ${props => props.justify || 'flex-start'};
  gap: ${props => props.gap || '0'};
`;

// Composed component
function UserCard({ user }) {
  return (
    <Box padding="1rem" background="white">
      <Flex align="center" gap="1rem">
        <Avatar src={user.avatar} />
        <Box>
          <Typography variant="h3">{user.name}</Typography>
          <Typography variant="body2" color="textSecondary">
            {user.email}
          </Typography>
        </Box>
      </Flex>
    </Box>
  );
}

In the above example, the Box and Flex components serve as building blocks with configurable props. The UserCard component composes these primitives to create a more complex UI element. This pattern reduces code duplication (since other components can now leverage Box and Flex) and creates a consistent API for spacing and layout throughout your application.

Performance Considerations

Be mindful of performance implications when choosing your styling approach:

  • With CSS-in-JS, avoid creating styled components inside render functions, as this creates new component instances on every render.
  • Use CSS variables for theme values that change frequently to avoid triggering React rerenders.
  • Leverage CSS Modules or Tailwind for static styles to avoid runtime overhead.
  • Consider code-splitting for large component libraries to reduce initial bundle size.

Accessibility

Don’t forget about accessibility! Verify that your styling choices support accessibility by providing adequate color contrast, visible focus states and respecting user preferences where applicable:

const Button = styled.button`
  /* Ensure sufficient color contrast */
  background-color: #007bff;
  color: white;
  
  /* Visible focus states */
  &:focus {
    outline: 2px solid #0056b3;
    outline-offset: 2px;
  }
  
  /* Respect user preferences */
  @media (prefers-reduced-motion: reduce) {
    transition: none;
  }
`;

In the simple example above, the focus outline helps keyboard users to see which element is active, while the prefers-reduced-motion media query respects users who have indicated they prefer reduced motion in their operating system settings.

Wrap-up

The React landscape today offers different styling solutions for different project types and team preferences. styled-components provides dynamic, component-scoped styling with excellent TypeScript support. CSS Modules offer a balanced approach with separated concerns and minimal runtime overhead. Tailwind CSS improves development speed with its utility-first philosophy. And component libraries like KendoReact accelerate development with production-ready components.

Remember that the best styling solution is one that helps you ship great user experiences while keeping your codebase maintainable and your team productive. Pick an approach that best aligns with your project’s requirements and team expertise.


And don’t forget: You can use KendoReact Free components for free, no strings, even in production.



Install Now


About the Author

Hassan Djirdeh

Hassan is a senior frontend engineer and has helped build large production applications at-scale at organizations like Doordash, Instacart and Shopify. Hassan is also a published author and course instructor where he’s helped thousands of students learn in-depth frontend engineering skills like React, Vue, TypeScript, and GraphQL.

Related Posts

Comments

Comments are disabled in preview mode.