DEV Community

Cathy Lai
Cathy Lai

Posted on • Edited on

Real World Tailwind CSS: Controlling the Special Cases (Part 2/2)

So in the last article, we have discussed the best practices for using Tailwind CSS4 in the real world - which is,

  • A dedicated team maintaining the components
  • The other teams just use them for creating pages
  • Linter to enforce boundaries, not good will

In this way we "hide" the long utility class strings from rest of the codebase.

Special Landing Page

What happens when marketing launches a new campaign and needs a gradient CTA button that deviates from the core design system??

The obvious choice might be to do something like this:

const variants = {
  primary: "...",
  secondary: "...",
  // The new marketing variant
  "ai-campaign": "bg-gradient-to-r from-emerald-500 to-teal-500 animate-pulse text-lg px-8 py-4" 
};
Enter fullscreen mode Exit fullscreen mode

This looks clean today. But in a few years, it might grow to

<Button
  variant="primary"
  isMarketing
  isAnimated
  hasGlowEffect
  showConfetti
  campaignTheme="black-friday-2024"
  seasonalBadge="sale"
  size="xl"
/>
Enter fullscreen mode Exit fullscreen mode

Now it's bloated again... this is called prop explosion.

What's the Best Way?

Option 1: Keep It Local with Tailwind Utilities

Sometimes a design is so specific that it's hard to imagine anyone using it again. So, just make the page live in a separate directory and use utility classes directory

////  Inside @/app/marketing/campaign/page.tsx ////
...
export default function CampaignPage() {
  return (
    <div>
      <h1>The Next Generation of AI</h1>

      {/* Custom, one-off visual classes are localized entirely to this page */}
      <Button
        className={cn("bg-indigo-600 text-white px-4 py-2", className)} >
        Get Early Access
      </Button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Once the campaign ends, just delete the page. The component is left untouched.

Option 2: Add a Variant

If there are only a handful of visual styles and they genuinely represent something the business uses repeatedly, I'd probably just add another variant.

<Button variant="primary" />
<Button variant="secondary" />
<Button variant="cta" />
<Button variant="voucher" />

The props felt easy enough to understand, and it keeps the page code nice and clean. In a smaller codebase, maintaining a few extra variants would not be a big deal.

Option 3: Create a Specialized Component

There's also a middle ground. If I find myself copying the same styling around a campaign or a particular section of the application, I'd probably start wondering whether it's worth creating something like a .


function CampaignButton({ className, ...props }) {
  return (
    <Button
      className={cn(
        "bg-gradient-to-r ...",
        className
      )}
      {...props}
    />
  );
}

// Base component
<Button>
  Testimonials
</Button>

// Campaign-specific component
<CampaignButton>
  Get Early Access
</CampaignButton>

That way the shared Button stays focused on being a button, while the campaign gets its own reusable abstraction. It feels like a reasonable compromise when you're not quite ready to promote something into the design system, but you don't want duplicated styling everywhere either.

The goal is:

Keep shared components focused, understandable, and maintainable.

Do you agree with the tradeoffs? What are your tips for writing even tidier and more maintainable components? Please share your experience ~ :)

Top comments (5)

Collapse
 
alexandersstudi profile image
Alexander

Marketing requests are easily the fastest way to ruin a clean component API. Scoping those bespoke utility strings entirely to the campaign route is the absolute right call. Once you merge a campaignTheme="black-friday" prop into a core component, you guarantee technical debt since nobody ever remembers to delete it in January. Building a separate <MarketingButton> that lives purely in that feature directory keeps the global design system completely naive to the chaos.

Collapse
 
jenny_akhi_aade503c2764f6 profile image
Jenny Akhi

Using dedicated teams for component maintenance is a game-changer for consistency. I’m curious, how are you handling the bundle size optimization when those 'special landing pages' inevitably start adding custom one-off utilities?

Collapse
 
cathylai profile image
Cathy Lai

That’s a great question. Personally, I’d be more concerned about maintainability than bundle size. Tailwind only generates the utility classes that are actually used, so a handful of campaign-specific utilities typically adds very little CSS.

In your experience, does it create a problem sometimes? Hopefully with Tailwind 4, the new Rust engine, it gets improved.

Collapse
 
jenny_akhi_aade503c2764f6 profile image
Jenny Akhi

You are absolutely right about the compiler keeping the .css file lean! To answer your question: the problem I’ve encountered isn't usually the raw CSS size, but rather the HTML payload bloat and specificity conflicts.
​When those one-off campaign utilities pile up, overriding them cleanly within base components often becomes a nightmare of unpredictable cascading rules. To solve this without adding runtime overhead, I actually ended up building an open-source tool called Traceless-Style. We solved this by injecting strict build-time metadata so the utility can handle deterministic class merging (you can check out how we handle it here: github.com/sparkgoldentech/tracele...
​But I completely agree maintainability is the top priority, and I'm definitely eager to see how the v4 Rust engine tackles these architecture challenges!

Thread Thread
 
cathylai profile image
Cathy Lai

That’s an interesting perspective! The class-merging and specificity side is definitely another challenge once teams start allowing overrides. I’ll have a look at Traceless-Style—thanks for sharing it!

I’ll keep researching on v4 and updating my findings here!