Styling Modern React Applications 2025
Modern React development has fundamentally shifted from page-based thinking to component-based architecture, yet CSS practices haven't always evolved accordingly. This article explores contemporary approaches to styling React applications, comparing utility-first CSS (Tailwind), component libraries (shadcn/ui vs Ant Design), and CSS Modules. It also considers the role of AI-assisted development and generative UI contexts like MCP-UI and OpenAI’s AgentKit.
Key Recommendation: For most modern React applications in 2025, a combination of Tailwind CSS with shadcn/ui provides the optimal balance of developer experience, maintainability, and AI compatibility.
1. The Component-First Philosophy
Why Traditional CSS Falls Short
The fundamental issue with traditional CSS in modern React is a conceptual mismatch. We build components, not pages, yet we often style globally with semantic classes that attempt to describe content rather than purpose.
As Alex Kondov articulates in How to style a React Application, traditional semantic classes like .essay or .text-box create several problems:
- Tight Coupling: CSS structure mirrors HTML structure, creating brittle dependencies
- Reusability Challenges: Classes become either too specific to reuse or too generic to maintain semantic meaning
- Scaling Issues: Each new variation requires architectural decisions about class hierarchy
- Design Token Enforcement: Magical hardcoded values proliferate despite good intentions
Thinking in Components, Not Classes
The paradigm shift is simple but profound: you're not reusing CSS classes, you're reusing components. When you need a button styled consistently across your app, you create a Button component, not a .btn class. The component encapsulates both behavior and appearance.
This realization eliminates the artificial separation between markup and styles that made sense in the jQuery era but adds friction in component-based development.
2. Utility-First CSS: The Tailwind Approach
Why Utility CSS Works
Tailwind CSS embraces the tight coupling between components and their styles, but inverts the traditional approach:
Traditional Approach:
// Semantic class in HTML
<div className="user-card">
<h2 className="user-card-title">John Doe</h2>
</div>
// Separate CSS file
.user-card { padding: 1rem; border: 1px solid gray; }
.user-card-title { font-size: 1.5rem; font-weight: bold; }
Utility-First Approach:
<div className="p-4 border border-gray-300 rounded">
<h2 className="text-2xl font-bold">John Doe</h2>
</div>
Key Benefits
-
Fixed Complexity: Styling effort remains constant as the application grows. No risk of unintentional style conflicts.
-
Design Tokens Built-In: Using
text-2xlorp-4automatically enforces your design system. No need for code review vigilance on hardcoded values. -
Colocation: Styles live with the markup, making changes faster and less error-prone. You never wonder "is this class used anywhere else?"
-
Readability Over Time: A year later,
flex items-center gap-4 p-6is immediately understandable without jumping to CSS files. -
Predictable Performance: No cascading style conflicts, minimal CSS bundle size due to purging unused classes.
Handling Complexity
For components with significant conditional styling:
import { clsx } from 'clsx';
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}
function Button({ variant = 'primary', size = 'md', disabled }: ButtonProps) {
return (
<button
className={clsx(
// Base styles
'font-semibold rounded transition-colors',
// Size variants
{
'px-3 py-1.5 text-sm': size === 'sm',
'px-4 py-2 text-base': size === 'md',
'px-6 py-3 text-lg': size === 'lg',
},
// Color variants
{
'bg-blue-600 hover:bg-blue-700 text-white': variant === 'primary',
'bg-gray-200 hover:bg-gray-300 text-gray-900': variant === 'secondary',
'bg-red-600 hover:bg-red-700 text-white': variant === 'danger',
},
// States
disabled && 'opacity-50 cursor-not-allowed'
)}
disabled={disabled}
>
{children}
</button>
);
}
3. Component Libraries: shadcn/ui vs Ant Design
The Philosophical Divide
These two libraries represent fundamentally different approaches to component libraries:
Ant Design: Traditional component library
- Pre-packaged, installable via npm
- Opinionated design language (Alibaba's design system)
- CSS-in-JS or Less for theming
- 65+ production-ready components
- Supports React, Angular, Vue
shadcn/ui: Component collection
- Copy-paste components into your codebase
- Built with Tailwind CSS and Radix UI primitives
- You own the code completely
- Minimal, customizable starting points
- React-only (TypeScript-first)
Detailed Comparison
| Aspect | shadcn/ui | Ant Design |
|---|---|---|
| Installation | Copy components individually | npm package dependency |
| Customization | Full control - you own the code | Theme configuration, CSS overrides |
| Bundle Size | Only what you use | Entire library (with tree-shaking) |
| Design Style | Minimal, adaptable | Enterprise-focused, polished |
| Learning Curve | Requires Tailwind knowledge | Well-documented API |
| Updates | Manual (you copy new versions) | Standard npm update |
| Accessibility | Built on Radix UI primitives | Good, but some reported issues |
| TypeScript | Excellent, first-class | Good support |
| AI Compatibility | Excellent (simple structure) | Moderate (complex API) |
When to Choose Each
Choose shadcn/ui when:
- Building a product requiring unique branding
- You want maximum control and flexibility
- Team is comfortable with Tailwind CSS
- Prioritizing modern development practices
- Working with AI coding assistants (Claude, Copilot, Cursor)
- Building SaaS products or startups
Choose Ant Design when:
- Building enterprise/internal tools quickly
- Need extensive, production-tested components
- Team prefers traditional component libraries
- Design consistency more important than customization
- Working with large teams requiring standardization
- Need cross-framework support (React, Vue, Angular)
The shadcn/ui Advantage in 2025
shadcn/ui has gained significant momentum because it aligns with modern development practices:
- Ownership: You control the components completely - no "magic" or black boxes
- Simplicity: Components are straightforward React + Tailwind - easy to understand and modify
- Composability: Built on Radix UI primitives, ensuring accessibility while allowing customization
- Developer Experience: Works seamlessly with modern tooling (Vite, Next.js, TypeScript)
- AI-Friendly: Simple, readable code structure makes it ideal for AI-assisted development
4. CSS Modules: When and Why?
What Are CSS Modules?
CSS Modules provide scoped CSS by automatically generating unique class names:
// Button.module.css
.button {
padding: 0.5rem 1rem;
background: blue;
}
// Button.tsx
import styles from './Button.module.css';
function Button() {
return <button className={styles.button}>Click me</button>;
}
// Rendered as:
// <button class="Button_button__x7k3m">Click me</button>
The Modern Reality
CSS Modules are increasingly less relevant in 2025 for most React applications. Here's why:
- Solved Problem: They solve style scoping, but Tailwind and component libraries already provide this
- Build Step Complexity: They require configuration and tooling
- Limited Reusability: Still suffer from the same reusability challenges as traditional CSS
- Developer Experience: Require jumping between files
- AI Assistance: Harder for AI to reason about styles split across files
When CSS Modules Still Make Sense
There are legitimate use cases:
- Migration Path: Gradually modernizing a legacy codebase
- Hybrid Approach: Using alongside Tailwind for complex, component-specific styles
- Team Preference: When team is strongly opposed to utility classes
- Specific Constraints: Projects with strict CSS-only requirements
Recommendation
For greenfield projects in 2025: Skip CSS Modules. Use Tailwind + shadcn/ui instead. For existing projects with CSS Modules: Consider them technical debt to eventually migrate away from, not expand.
5. AI-Assisted Development Considerations
Why This Matters
AI coding assistants (Claude, GitHub Copilot, Cursor) have become standard tools. Your styling approach significantly impacts AI effectiveness.
AI-Friendliness Comparison
Most AI-Friendly: Tailwind CSS
- Simple, declarative utility classes
- Self-documenting (classes describe exactly what they do)
- No context switching between files
- Easy for AI to reason about and modify
- Consistent patterns across projects
Moderately AI-Friendly: shadcn/ui
- Clear component structure
- TypeScript-first makes intent obvious
- Self-contained components
- AI can easily suggest modifications
Least AI-Friendly: Complex CSS Architectures
- CSS Modules require understanding file relationships
- Ant Design's extensive API requires deep documentation knowledge
- Custom CSS architectures need project-specific context
Practical Example
AI Prompt with Tailwind:
"Make this button larger and add a hover effect"
AI easily understands and modifies:
// From:
<button className="px-4 py-2 bg-blue-500">
// To:
<button className="px-6 py-3 bg-blue-500 hover:bg-blue-600 transition">
AI Prompt with CSS Modules:
"Make this button larger and add a hover effect"
AI must reason across files:
// Button.tsx - update className
// Button.module.css - modify .button class
// Need to understand both files and their relationship
Best Practices for AI-Assisted Development
- Use Inline Styles (Tailwind): Keep everything in one file for AI context
- TypeScript Everything: Helps AI understand component contracts
- Clear Component Names:
UserProfileCardnotCard - Consistent Patterns: Follow established conventions
- Documentation: Brief comments for complex logic
6. Generative & Adaptive UIs
The New Frontier
Generative UIs render dynamically based on user needs, often in sandboxed environments. Two major approaches:
MCP-UI (Model Context Protocol UI)
Architecture:
- Server returns UI resources (HTML, React components)
- Client renders in sandboxed iframe
- Security-first design
Styling Implications:
import { createUIResource } from '@mcp-ui/server';
const interactiveForm = createUIResource({
uri: 'ui://user-form/1',
content: {
type: 'externalUrl',
iframeUrl: 'https://yourapp.com/form'
}
});
Key Challenge: Iframe isolation means limited access to parent styles.
Solution: Self-contained components with Tailwind CDN:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div class="p-6 bg-white rounded-lg shadow-lg">
<!-- Component content -->
</div>
</body>
</html>
OpenAI AgentKit / Apps SDK
Architecture:
- React components in iframe
- Communication via
window.openaiAPI - Build step produces single JS bundle
Example Component:
import React, { useState, useEffect } from 'react';
export default function PizzaList() {
const toolOutput = window.openai?.toolOutput;
const [favorites, setFavorites] = useState<string[]>([]);
useEffect(() => {
if (toolOutput?.favorites) {
setFavorites(toolOutput.favorites);
}
}, [toolOutput]);
return (
<div className="p-4 space-y-4">
{toolOutput?.places?.map(place => (
<div
key={place.id}
className="border rounded-lg p-4 hover:shadow-lg transition"
>
<h3 className="text-xl font-bold">{place.name}</h3>
<p className="text-gray-600">{place.description}</p>
</div>
))}
</div>
);
}
Styling Requirements:
- Self-Contained: All styles inline (Tailwind perfect for this)
- Responsive: Must work at various sizes (inline, PiP, fullscreen)
- Theme-Aware: Respect host's theme (light/dark mode)
- Minimal Bundle: Keep dependencies lean
Why Tailwind Dominates Here
For generative UIs, Tailwind CSS is nearly essential:
- No External CSS Files: Everything inline
- Predictable: No cascade issues from host environment
- Responsive Built-In:
md:,lg:prefixes handle layout modes - Small Bundle: Only includes used utilities
- Theme Variables: Easy to respect host theme
- AI Generation: AI can generate complete UI with just HTML + Tailwind
Example: Theme-Aware Component
function AdaptiveCard() {
const theme = window.openai?.theme || 'light';
return (
<div className={`
p-6 rounded-lg
${theme === 'dark'
? 'bg-gray-800 text-white'
: 'bg-white text-gray-900'}
`}>
<h2 className="text-2xl font-bold mb-4">
Adaptive Content
</h2>
</div>
);
}
7. Practical Recommendations
For New React Projects (2025)
Recommended Stack:
- Build Tool: Vite or Next.js
- Styling: Tailwind CSS
- Components: shadcn/ui
- Icons: lucide-react (included with shadcn/ui)
- Forms: React Hook Form + Zod
- Utilities: clsx or cva (class-variance-authority)
Setup:
# Create new project
npm create vite@latest my-app -- --template react-ts
cd my-app
# Install Tailwind
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
# Install shadcn/ui
npx shadcn-ui@latest init
# Add components as needed
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
Component Architecture Pattern
// components/ui/button.tsx (from shadcn/ui)
// You own this code - modify freely
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
For Enterprise Applications
Consider:
- Ant Design if you need comprehensive, battle-tested components immediately
- Tailwind + shadcn/ui if you can invest time in building your design system
- Hybrid Approach: Ant Design for complex components (tables, forms), Tailwind for layout and custom components
For Generative/Adaptive UIs
Must-Have:
- Tailwind CSS (self-contained styling)
- Minimal dependencies
- Bundle size optimization
- Theme-aware design patterns
Architecture:
// Build a component library specifically for generative contexts
// components/adaptive/Card.tsx
interface CardProps {
theme?: 'light' | 'dark';
maxHeight?: number;
}
export function Card({ theme, maxHeight, children }: CardProps) {
return (
<div
className={`
p-6 rounded-lg shadow-lg
${theme === 'dark' ? 'bg-gray-800 text-white' : 'bg-white text-gray-900'}
`}
style={{ maxHeight }}
>
{children}
</div>
);
}
Migration Strategy (Legacy → Modern)
If you have existing CSS/CSS Modules:
- Phase 1: Add Tailwind, use for new components
- Phase 2: Gradually convert high-traffic components
- Phase 3: Extract common patterns into shadcn/ui-style components
- Phase 4: Remove old CSS once coverage is sufficient
Don't rewrite everything at once - parallel systems can coexist.
8. Anti-Patterns to Avoid
1. Mixing Too Many Approaches
// ❌ Bad - Three styling systems in one component
import styles from './card.module.css';
import { Button } from 'antd';
function Card() {
return (
<div className={`${styles.card} p-4 border`}>
<Button type="primary">Click</Button>
</div>
);
}
2. Fighting the Framework
// ❌ Bad - Custom CSS overriding Tailwind
<div className="p-4" style={{ padding: '20px' }}>
// ✅ Good - Use Tailwind's scale
<div className="p-5">
3. Over-Engineering Abstraction
// ❌ Bad - Premature abstraction
const SPACING = {
xs: 'p-1',
sm: 'p-2',
md: 'p-4',
lg: 'p-6',
};
// ✅ Good - Use Tailwind directly until patterns emerge
<div className="p-4">
4. Inline Styles for Complex Logic
// ❌ Bad - Complex calc() in inline styles
<div style={{ width: 'calc(100% - 64px)' }}>
// ✅ Good - Use Tailwind or component variant
<div className="w-full pr-16">
9. Performance Considerations
Bundle Size Comparison
Typical Production Builds:
- Tailwind CSS (purged): ~5-20 KB
- shadcn/ui components: ~2-5 KB per component (you only bundle what you use)
- Ant Design: ~200-500 KB (with tree-shaking)
- CSS Modules: Depends on how much CSS you write
Optimization Tips
Tailwind Purging (automatic in modern setups):
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
// Only classes used in these files are included
};
Component Lazy Loading:
const HeavyChart = lazy(() => import('./components/HeavyChart'));
CSS-in-JS Caution: Runtime CSS-in-JS (styled-components, Emotion without extraction) adds overhead. Avoid unless necessary.
Conclusion
The modern React styling landscape has consolidated around utility-first CSS, particularly Tailwind, combined with flexible component systems like shadcn/ui. This approach:
- Aligns with component-based thinking
- Scales predictably with application size
- Works seamlessly with AI coding assistants
- Supports generative/adaptive UI contexts
- Provides excellent developer experience
- Delivers optimal runtime performance
For most React developers in 2025, the winning combination is Tailwind CSS + shadcn/ui + TypeScript
This stack provides the right balance of productivity, maintainability, and flexibility for modern application development.
Additional Resources
- Tailwind CSS Documentation
- shadcn/ui Documentation
- Radix UI Primitives
- MCP-UI Specification
- OpenAI Apps SDK
- Alex Kondov: How to style a React Application
Building scalable React applications with TypeScript is what I do. If you need production-grade components, real-time visualizations, or AI-integrated interfaces that actually ship, let's talk. Get in touch to discuss your requirements.