DEV Community

ridbyte
ridbyte

Posted on • Originally published at ridbyte.com

Why You Should Always Prefix Tailwind in Embeddable Widgets

Why You Should Always Prefix Tailwind in Embeddable Widgets

I'm building CommentBy, a commenting widget that embeds anywhere with a single <div> tag. I learned the hard way: if you use Tailwind in an embeddable widget, prefix your classes from day one.

Without prefixing, my widget's styles conflicted with every site that also used Tailwind. Buttons disappeared, layouts broke, some sites looked completely broken.

Here's the problem and the fix.

The Problem with Unprefixed Tailwind in Widgets

Your widget loads on someone else's site:

<!-- User adds this to their site -->
        <div id="commentby_comment_box"></div>
        <script defer src="https://cdn.commentby.com/widget.js"></script>
Enter fullscreen mode Exit fullscreen mode

Your code renders HTML with Tailwind classes into their page. If they also use Tailwind, you have collision problems:

Scenario 1: Different theme configurations

  • Their Tailwind defines blue-500 as #1E40AF
  • Your Tailwind defines blue-500 as #3B82F6
  • Result: Your blue buttons are the wrong shade of blue

Scenario 2: Different Tailwind versions

  • They use Tailwind 2.x with different spacing scale
  • You use Tailwind 3.x/4.x
  • Result: Your p-4 has different padding than expected

Scenario 3: Custom color palettes

  • They redefined gray-100 to be pink for their brand
  • You use bg-gray-100 for comment cards
  • Result: Pink comment cards

Scenario 4: Global style overrides (This was the worst)

/* Host site has this */
button {
  background: transparent;
  border: 2px solid black;
  font-weight: bold;
}
Enter fullscreen mode Exit fullscreen mode

Your widget's bg-blue-500 button suddenly has transparent background because their global styles override your utilities (unless you use !important).

Real Examples from CommentBy

Tech blog (Next.js + Tailwind):

  • Used custom gray palette
  • Result: My light gray backgrounds became bright pink

WordPress site:

  • Theme had button { all: unset; } reset
  • Result: My submit button became invisible

Ghost blog:

  • Used !important on base text colors
  • Result: All my text hierarchy broke

Marketing site:

  • Had global * { box-sizing: content-box; }
  • Result: All padding calculations broke, layout collapsed

The Solution: Prefix Everything

Use Tailwind's prefix feature from day one:

/* widget.css */
@import "tailwindcss" prefix(commentby) important;
Enter fullscreen mode Exit fullscreen mode

Two directives:

  1. prefix(commentby) - All classes get commentby: namespace
  2. important - All utilities get !important to override host styles

Now your classes look like:

// Before (dangerous)
<div className="bg-white p-4 rounded-lg shadow-md">
  <button className="bg-blue-500 px-6 py-2 text-white">
    Submit
  </button>
</div>

// After (safe)
<div className="commentby:bg-white commentby:p-4 commentby:rounded-lg commentby:shadow-md">
  <button className="commentby:bg-blue-500 commentby:px-6 commentby:py-2 commentby:text-white">
    Submit
  </button>
</div>
Enter fullscreen mode Exit fullscreen mode

Your .commentby\:bg-blue-500 won't conflict with their .bg-blue-500.

How Prefix Works

The prefix goes first, before all modifiers:

<div className="
  commentby:p-4
  commentby:md:p-6
  commentby:lg:p-8
  commentby:hover:bg-blue-500
  commentby:focus:ring-2
  commentby:dark:bg-gray-800
">
Enter fullscreen mode Exit fullscreen mode

Not md:commentby:p-6 - the prefix is always first.

Generated CSS

Tailwind compiles to:

.commentby\:bg-blue-500 {
  background-color: rgb(59 130 246) !important;
}

.commentby\:p-4 {
  padding: 1rem !important;
}

@media (min-width: 768px) {
  .commentby\:md\:p-6 {
    padding: 1.5rem !important;
  }
}

.commentby\:hover\:bg-blue-600:hover {
  background-color: rgb(37 99 235) !important;
}
Enter fullscreen mode Exit fullscreen mode

Every utility gets:

  • Your prefix namespace (.commentby\:)
  • !important flag
  • Proper CSS escaping (\: for colons)

Why important is Critical

Without !important, host page styles still win:

/* Their site */
button {
  background: transparent;
  padding: 10px;
}

/* Your widget */
.commentby\:bg-blue-500 {
  background-color: rgb(59 130 246); /* Lost - no !important */
}
Enter fullscreen mode Exit fullscreen mode

Global button styles have higher specificity. Your button stays transparent.

With important, you win:

.commentby\:bg-blue-500 {
  background-color: rgb(59 130 246) !important; /* Wins */
}
Enter fullscreen mode Exit fullscreen mode

Implementation with Preact

Here's how I use it in CommentBy:

// Widget.jsx
import { render } from 'preact';

function CommentWidget() {
  return (
    <div className="commentby:bg-white commentby:p-6 commentby:rounded-lg commentby:shadow-lg">
      <form className="commentby:space-y-4">
        <textarea 
          className="commentby:w-full commentby:p-3 commentby:border commentby:rounded"
          placeholder="Add a comment..."
        />
        <button 
          className="commentby:bg-blue-500 commentby:text-white commentby:px-4 commentby:py-2 commentby:rounded commentby:hover:bg-blue-600"
        >
          Submit
        </button>
      </form>
    </div>
  );
}

// Mount to the page
const container = document.getElementById('commentby-widget');
render(<CommentWidget />, container);
Enter fullscreen mode Exit fullscreen mode

CSS gets bundled into the same file with Vite, so it's one JavaScript file with inlined styles.

Shadow DOM Alternative

You could use Shadow DOM for complete isolation:

const container = document.getElementById('commentby-widget');
const shadowRoot = container.attachShadow({ mode: 'open' });

// Render Preact into Shadow DOM
render(<CommentWidget />, shadowRoot);
Enter fullscreen mode Exit fullscreen mode

With Shadow DOM, you don't need prefix or important - styles are fully isolated.

Why I didn't use Shadow DOM:

  • Can't inherit host site's font (looks disconnected)
  • Can't inherit base text colors (feels like iframe)
  • More complex event handling
  • Accessibility concerns
  • Want widget to feel integrated, not isolated

The prefix + important approach gives isolation where needed while allowing intentional inheritance.

Testing Strategy

Test your widget on sites with different CSS:

  1. Tailwind site (class conflicts)
  2. Bootstrap site (different utilities)
  3. WordPress with theme (global button styles)
  4. Ghost blog (custom CSS)
  5. Legacy site (old CSS, weird resets)
  6. Minimal site (baseline)

For each, verify:

  • Layout integrity
  • Colors match design
  • Spacing correct
  • Buttons render properly
  • Responsive breakpoints work

Performance

Prefix adds minimal overhead:

  • Without prefix: 45KB minified
  • With prefix + important: 47KB minified

The 2KB increase (just the prefix string repeated) is worth it for zero conflicts across any site.

What I Wish I Knew

Start with prefix from day one. I had to refactor 150+ component files, updating every single className. It's tedious and error-prone.

Don't assume your test page represents real sites. Clean HTML with no CSS is nothing like a production WordPress site with 15 plugins and aggressive theme styles.

Test early on real sites with real CSS conflicts.

Conclusion

If you're building embeddable widgets with Tailwind:

  1. Use prefix from day one: @import "tailwindcss" prefix(yourapp) important;
  2. Prefix syntax: yourapp:p-4, yourapp:md:p-6, yourapp:hover:bg-blue-500
  3. Always include important to override host styles
  4. Test on real sites early (multiple CSS frameworks, versions, custom styles)
  5. Consider Shadow DOM only if you need total isolation

Don't learn this the hard way like I did. Prefix from the start.


Building CommentBy - a privacy-first commenting widget that embeds anywhere. Built with Preact and properly prefixed Tailwind. Try the demo.

What's your experience with CSS conflicts in embeddable widgets? Drop a comment.

Top comments (0)