I’ve spent way too many hours of my life debugging CSS. Not because the problems are hard, but because CSS is fundamentally chaos. You change one thing over here, and something completely unrelated breaks over there. Cascade, specificity, inheritance - it’s a house of cards.
We started the vehcology project with Pico CSS. It seemed like a good idea at the time. Minimal, semantic, just add some classes and you’re done. Clean HTML, reasonable defaults, no build step.
Six months later, we were drowning in custom CSS overrides, fighting specificity battles, and spending more time tweaking margins than building features.
So we ripped it all out and went full Tailwind + DaisyUI.
The Pain Points
Pico is great for simple sites. But our evolution dashboard wasn’t simple. We had:
- Nested component layouts with conditional styling
- Responsive grids that changed based on viewport and data
- Dark mode that needed to cascade through custom components
- Interactive visualizations with dynamic state
- Modal dialogs, tabs, pagination, data tables
Every new feature meant more custom CSS. And custom CSS meant more conflicts. We’d fix the spacing in one component and break it in another. We’d add a utility class and discover it clashed with Pico’s defaults.
The breaking point was the data tables. We needed sortable columns, filterable rows, pagination controls, and responsive layouts. Pico gave us nice semantic table styling, but no utilities for interactive behavior. We ended up writing hundreds of lines of custom CSS to handle hover states, active sorts, and mobile layouts.
It was chaos.
The Decision
Tailwind felt like the opposite of everything I believed about CSS. Utility classes everywhere? That’s just inline styles with extra steps!! What happened to separation of concerns?
But here’s the thing: utility-first CSS doesn’t fight the cascade. It embraces it. Every class does exactly one thing. No hidden side effects. No surprises. You see all the styles right there in the HTML.
DaisyUI adds component classes on top of Tailwind, giving you the semantic sugar when you want it. Need a button? class="btn btn-primary". Need a custom button? Add Tailwind utilities. Best of both worlds.
The combination was compelling enough to try.
The Migration
We didn’t do a gradual migration. We couldn’t. Mixing Pico and Tailwind would’ve been worse than either alone. So we:
- Ripped out Pico entirely
- Installed Tailwind + DaisyUI
- Converted components one by one
- Deleted custom CSS files as we went
The first component took an hour. The second took thirty minutes. By the tenth, we were flying.
Here’s what a table header looked like before:
<th class="sortable" data-column="fitness">
Fitness
<span class="sort-indicator"></span>
</th>
With custom CSS:
.sortable {
cursor: pointer;
user-select: none;
position: relative;
}
.sortable:hover {
background-color: var(--hover-bg);
}
.sort-indicator {
position: absolute;
right: 0.5rem;
opacity: 0;
}
.sortable.active .sort-indicator {
opacity: 1;
}
/* Plus media queries for responsive behavior... */
After:
<th class="cursor-pointer select-none hover:bg-base-200 relative"
data-column="fitness">
Fitness
<span class="absolute right-2 opacity-0 group-hover:opacity-100">↓</span>
</th>
No separate CSS file. No naming conventions. No specificity wars. Just classes that do what they say.
The Benefits
The immediate win was development speed. Adding a new component went from “write HTML, write CSS, debug CSS, curse at CSS” to just “write HTML with utility classes.” Done.
The second win was consistency. DaisyUI’s theme system meant all our buttons, inputs, and cards shared the same design language. No more “why is this button 2px taller than that one?”
The third win was dark mode. With Pico, we had custom CSS variables for colors that needed updating everywhere. With Tailwind, we swapped the DaisyUI theme and everything just worked. Text colors, backgrounds, borders - all handled automatically.
The fourth win was responsive design. Tailwind’s breakpoint prefixes (sm:, md:, lg:) made it trivial to adjust layouts. Want a grid that’s 1 column on mobile, 2 on tablet, 3 on desktop? grid-cols-1 md:grid-cols-2 lg:grid-cols-3. Done.
The Tradeoffs
The HTML got verbose. Really verbose. Some components have 15+ utility classes on a single element. That’s a lot to read.
But here’s what we gained: you can understand the styling without leaving the file. No jumping to CSS files. No wondering if there’s a global style affecting things. It’s all right there.
The bundle size increased. Tailwind generates a lot of CSS, even with purging. But our app was already JavaScript-heavy. An extra 50KB of CSS wasn’t the bottleneck.
And we lost some semantic clarity. <button class="btn"> is more readable than <button class="px-4 py-2 rounded bg-blue-500 text-white">. DaisyUI helped here - we used component classes for common patterns and utility classes for customization.
The Outcome
Three weeks after starting the migration, we’d converted every component. Our custom CSS went from ~2000 lines to ~50 lines (for a few truly unique cases).
New features got built faster. The evolution dashboard that took a week to style with Pico took two days with Tailwind. The run detail page took an afternoon.
And when we needed to update the design system? Changed the DaisyUI theme config. Pushed to production. Everything updated at once.
No CSS debugging. No specificity issues. No cascade surprises.
Just utility classes all the way down.
The Lesson
Utility-first CSS isn’t about abandoning separation of concerns. It’s about recognizing that CSS is the concern, and keeping it close to the HTML makes it manageable.
The cascade is powerful, but it’s also unpredictable. Utility classes trade global complexity for local verbosity. And in a complex application, that’s a trade worth making.
Pico is great for content sites. Tailwind + DaisyUI is great for applications. Know which one you’re building.
We’re building an application. Tailwind won.