Component First Strategy
The Problem
When projects use a utility-first CSS framework like Tailwind CSS alongside a component framework (React, Vue, Svelte, etc.), developers and AI agents frequently fall back to traditional CSS patterns. Instead of composing utility classes inside components, they create custom CSS class names — .profile-card, .btn-primary, .sidebar-nav — with separate stylesheets or CSS modules.
This creates a fragmented codebase:
- Some components use Tailwind utilities inline
- Others introduce custom CSS classes with BEM naming or CSS modules
- Some mix both approaches in the same file
The inconsistency makes the project harder to maintain. You can never be sure whether a piece of UI is styled with utilities, custom CSS, or a mix of both.
For AI agents, this is a particularly common failure mode. Given a task like “build a profile card,” an agent will often generate a .profile-card class with a CSS module — the pattern seen most often in training data. Even in a project that uses Tailwind exclusively, AI-generated code introduces custom CSS classes that drift from the project’s conventions. Over time, the codebase becomes a patchwork of conflicting styling approaches.
The Solution
Component First Strategy: when your project uses a component-based framework with a utility CSS framework, always express UI as components with utility classes. Never create UI-level CSS class names with separate stylesheets.
- Need a card? Create a
<Card>component with utility classes - Need a button variant? Create a
<Button variant="primary">component - Need a layout pattern? Create a
<PageLayout>component
The component itself is the abstraction. CSS class names like .card or .btn-primary are unnecessary — the component handles encapsulation, and utility classes handle styling.
Code Examples
Anti-Pattern: Custom CSS Classes in a Tailwind Project
This is what you should not do in a component-based project with Tailwind. The developer has created custom CSS class names and a separate stylesheet — bypassing the utility framework entirely:
// ProfileCard.module.css
// .profileCard { display: flex; gap: 1rem; padding: 1.5rem; ... }
// .avatar { width: 64px; height: 64px; border-radius: 50%; ... }
// .name { font-size: 1.25rem; font-weight: 600; ... }
// .role { color: #6b7280; font-size: 0.875rem; ... }
import styles from './ProfileCard.module.css';
function ProfileCard({ name, role, avatar }) {
return (
<div className={styles.profileCard}>
<img className={styles.avatar} src={avatar} alt="" />
<div>
<h3 className={styles.name}>{name}</h3>
<p className={styles.role}>{role}</p>
</div>
</div>
);
}
The CSS for this anti-pattern looks like traditional component CSS — custom class names, separate file, BEM-influenced naming:
This works visually, but introduces naming decisions, a separate CSS file, and a styling approach that conflicts with the rest of the Tailwind-based project.
Recommended: Component First with Utility Classes
The same result, achieved with utility classes composed directly inside the component:
function ProfileCard({ name, role, avatar }) {
return (
<div className="flex gap-4 p-6 bg-white rounded-lg shadow-md">
<img
className="w-16 h-16 rounded-full object-cover"
src={avatar}
alt=""
/>
<div className="flex flex-col justify-center">
<h3 className="text-xl font-semibold text-slate-800">{name}</h3>
<p className="text-sm text-gray-500 mt-1">{role}</p>
</div>
</div>
);
}
No CSS file. No class names to invent. The component encapsulates the visual design. When you need to change the card’s look, you edit one file — the component.
Component Variants with Props
Instead of creating CSS modifier classes (.btn--primary, .btn--secondary), use component props to control variants:
function Button({ variant = 'primary', children, ...props }) {
const styles = {
primary: 'bg-blue-500 hover:bg-blue-700 text-white',
secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-800',
outline:
'bg-transparent hover:bg-blue-50 text-blue-600 border border-blue-500',
};
return (
<button
className={`${styles[variant]} font-semibold py-2 px-4 rounded`}
{...props}
>
{children}
</button>
);
}
Usage is self-documenting:
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="outline">Details</Button>
No .btn-primary class to maintain. The variant prop can be type-checked with TypeScript, documented with JSDoc, and auto-completed in your editor.
The demo below uses CSS classes to approximate the visual output. In a real project, the variant logic lives in the component code and utility classes handle the styling — no custom CSS classes are created.
Component Composition
Complex layouts are built by composing smaller components — not by adding more CSS classes:
function UserList({ users }) {
return (
<div className="divide-y divide-gray-200">
{users.map((user) => (
<div key={user.id} className="flex items-center gap-4 py-3">
<Avatar src={user.avatar} size="sm" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{user.name}
</p>
<p className="text-sm text-gray-500 truncate">{user.email}</p>
</div>
<Badge variant={user.status}>{user.status}</Badge>
</div>
))}
</div>
);
}
Each piece — <Avatar>, <Badge>, the list layout — is a component. No .user-list__item, .user-list__avatar, or .user-list__badge class names needed.
Responsive Patterns in Components
Utility frameworks use breakpoint prefixes for responsive behavior. These go directly in the component markup:
function ProductGrid({ products }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{products.map((product) => (
<ProductCard key={product.id} {...product} />
))}
</div>
);
}
No separate CSS file for the grid. No .product-grid or .product-grid--responsive class. The responsive behavior is declared inline, visible in the same place as the markup.
When Components Are Not Available
The component-first approach requires the ability to create reusable components. In some situations this is not possible:
- Server-rendered HTML from a CMS — the markup is fixed, you can only add stylesheets
- Third-party UI frameworks — libraries that emit fixed HTML you cannot modify
- Email templates — limited to inline styles and table layouts
- Static HTML sites without a build step — no component framework available
In these cases, fall back to other CSS strategies:
| Situation | Recommended Approach |
|---|---|
| Have a build tool, can’t change HTML | CSS Modules or Tailwind @apply |
| No build tool, global CSS only | BEM naming convention |
| Legacy codebase being migrated | Incremental component extraction |
These are exceptions, not the default. If you are working in React, Vue, Svelte, Astro, or any framework that supports components, the component-first approach should be your default choice.
Pros and Cons
Pros
- No naming decisions. The component name is the abstraction — no
.card-headeror.btn-primaryto debate. - Single source of truth. Style and markup live in one file. Change the component, change everywhere it is used.
- AI-friendly. AI agents can generate utility-based components reliably without guessing project-specific naming conventions.
- No CSS file management. No separate stylesheets, no dead CSS, no import chains.
- Props replace modifiers.
variant="primary"is clearer than.btn--primaryand supports TypeScript type-checking. - Design consistency. Utilities are tied to a design token scale (e.g.,
p-4=1rem,p-6=1.5rem), enforcing consistent spacing and sizing.
Cons
- Verbose class lists. Long utility strings in JSX can look cluttered. However, from 2026 onward, AI writes the code. Helpers like
clsx/cnexist for human readability — there is no need to add them as dependencies in new projects. If an existing project already uses them, keep them, but do not introduce them for new work. - Learning curve. Developers unfamiliar with the utility framework need to learn its vocabulary.
- Requires a component framework. Not applicable in plain HTML/CSS environments (see exceptions above).
Component-Tier Variables Are Unnecessary
In tiered design token strategies — such as a three-tier color strategy or a three-tier font-size strategy — the most concrete level is the component tier: CSS custom properties scoped to a specific component. For example, --_dialog-side-spacing, --_card-shadow, or --_nav-font-size (where the --_ prefix signals local scope). These variables let a component encapsulate its own design decisions in its CSS file.
With the component-first approach, this tier is unnecessary. There is no separate CSS file per component where you would define scoped custom properties. Styling is expressed as utility classes directly in the component’s markup, and the component framework itself (React, Vue, Svelte, Astro) provides the scoping boundary.
A <Dialog> component does not need a --_dialog-side-spacing variable — it uses px-hsp-sm or px-hsp-md directly (project-defined tokens from the tight token strategy). A <Card> does not need --_card-shadow — it uses shadow-md. The component file is already the single source of truth for that component’s styling decisions. The higher tiers (palette, theme, scale) still exist as global design tokens — only the component tier is eliminated, because the component architecture itself handles scoping.
In contrast, when styling with general CSS approaches (BEM, CSS Modules), component-tier variables remain valuable. Without a component framework providing scoping, CSS custom properties are one of the few mechanisms for encapsulating design decisions within a component’s stylesheet.
When to Use
Always use the component-first approach when:
- Your project uses a component framework (React, Vue, Svelte, Astro, Solid, etc.)
- Your project uses a utility CSS framework (Tailwind CSS, UnoCSS, etc.)
Fall back to other approaches only when:
- HTML cannot be changed (CMS output, third-party widgets, legacy markup)
- No component framework is available (static HTML, email templates)
- The project has explicitly chosen a different CSS convention and you should not mix approaches
Rules for AI Agents
When generating code for a component-based project with Tailwind:
- Always create components — not CSS classes
- Use utility classes directly in the component markup
- Never generate CSS module files or custom class names unless explicitly asked
- Use props for variants — not CSS modifiers like
.btn--primary - Compose components — build complex UI from smaller components, not from more CSS
Popular Tools
- Tailwind CSS — The most popular utility-first framework. Uses a build step to generate only the CSS you use.
- UnoCSS — An on-demand, fast alternative with a plugin-based architecture. Compatible with Tailwind presets.
- clsx / tailwind-merge — Helpers for composing conditional utility classes. Useful in existing projects but unnecessary as a new dependency — AI tooling handles verbose class strings without readability helpers.