A Way to Make Composable Tailwind Components

Jul 17, 2026 - Updated Jul 27, 2026

When you create a component with Tailwind styling, you run into a problem with combining classes. The following doesn’t work.

function Foo({ className }) {
  return <div className={`p-2 ${className || ""}`}></div>;
}

<Foo className="p-4">

If the styles collide, the class that appears last in the CSS takes precedence; the order in the class string is irrelevant.

Without help from the browser or the natural cascade of CSS, the problem is commonly solved with Javascript like so.

function Foo({ className }) {
  return <div className={twMerge("p-2", className)}></div>;
}

twMerge tries its best to remove classes on the left that should be overriden by classes on the right. But this problem is solvable without Javascript; it’s not dynamic and nothing changes at runtime.

The existing component classes should always be overriden. In other words, the component classes should have lower CSS precedence.

Here’s an example using CSS layers supported by the official Tailwind implementation.

@layer components {
  .foo {
    padding: 0.5rem;
  }
}
function Foo({ className }) {
  return <div className={`foo ${className || ""}`}></div>;
}

The only problem is, we still want to use regular utility classes to style components. This is where a Javascript-ecosystem Tailwind implementation like UnoCSS shines. It can modify classes during the build step.

Here’s a straightforward version that duplicates classes into a lower layer. UnoCSS’s variant groups makes this easy to write.

function Foo({ className }) {
  return <div className={`layer-name:(p-2 m-2) ${className || ""}`}></div>;
}

// UnoCSS expands grouped classes during build
function Foo({ className }) {
  return (
    <div className={`layer-name:p-2 layer-name:m-2 ${className || ""}`}></div>
  );
}
@layer name {
  .layer-name\:p-2 {
    padding: 0.5rem;
  }
  .layer-name\:m-2 {
    margin: 0.5rem;
  }
}

Here’s an interesting alternative using the UnoCSS’s class compilation.

function Foo({ className }) {
  return <div className={`:uno: p-2 m-2 ${className || ""}`}></div>;
}

// Static classes following :uno: get compiled during build
function Foo({ className }) {
  return <div className={`uno-so8f74 ${className || ""}`}></div>;
}
.uno-so8f74 {
  margin: 0.5rem;
  padding: 0.5rem;
}
/* utility classes below */

Since components are often reused, this may result in a smaller bundle size; only the final class name is repeated compared to a potentially giant class string.

Here’s an example of the configuration required to setup the variant groups and class compilation.

// uno.config.ts
import {
  defineConfig,
  presetWind4,
  transformerCompileClass,
  transformerVariantGroup,
} from "unocss";

export default defineConfig({
  presets: [presetWind4()],
  transformers: [transformerVariantGroup(), transformerCompileClass()],
});