• Skip to primary navigation
  • Skip to main content
  • Skip to footer

Codemotion Magazine

We code the future. Together

  • Discover
    • Events
    • Community
    • Partners
    • Become a partner
    • Hackathons
  • Magazine
    • Backend
    • Frontend
    • AI/ML
    • DevOps
    • Dev Life
    • Soft Skills
    • Infographics
  • Talent
    • Discover Talent
    • Jobs
    • Manifesto
  • Companies
  • For Business
    • EN
    • IT
    • ES
  • Sign in

jmlwebJanuary 21, 2025 2 min read

Building Agnostic Components with CVA and TailwindDesign/UX

Design/UX
css repos
facebooktwitterlinkedinreddit

Is Tailwind great? Yes. But does it have any weak points?

Using Tailwind offers numerous advantages:

Recommended article
web accessibility, accessibilità, WCAG
October 27, 2023

Making the Web Accessible to Everyone

Codemotion

Codemotion

Design/UX
  • Speeds up development: It keeps content and presentation layers close, avoids the cognitive load of naming classes, and nearly eliminates style collisions.
  • Enhances consistency: Through the use of tokens.
  • Produces small CSS files: By extracting only the classes you use.

However, it has a clear downside compared to traditional CSS approaches: code repetition. To mitigate this, Tailwind’s documentation suggests using components wherever possible.


Another drawback: lost semantics
For instance, applying the class btn--lg to an element implies using a “large” variant. However, using classes like px-6 py-4 text-lg loses this context.


How to solve this with React?

One way to address this at the UI library level (e.g., in React) is to create a Button component that accepts variants as props and maps those props to Tailwind classes.

<code>import type { ReactNode } from 'react';

const sizeStyles = {
  sm: 'px-3 py-1 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-4 text-lg',
};

const intentStyles = {
  primary: 'bg-blue-600 text-blue-50',
  secondary: 'bg-orange-600 text-orange-50',
  default: 'text-slate-600 text-slate-50',
};

interface Props {
  size: keyof typeof sizeStyles;
  intent: keyof typeof intentStyles;
  children: ReactNode;
}

export const Button = ({ size, intent, children }) => (
  <button className={`inline-block border-0 ${sizeStyles[size]} ${intentStyles[intent]}`}>
    {children}
  </button>
);
</code>Code language: JavaScript (javascript)

Advantages of this approach:

  1. Reduces repetitive Tailwind classes: No need to write the same classes repeatedly for buttons.
  2. Improves clarity: You don’t have to remember that primary equals bg-blue-600 text-blue-50.
  3. Centralizes styles: Updating styles is straightforward without needing to navigate multiple files.

But is this the final solution? Not quite.

This component has some limitations:

  • It doesn’t allow passing an onClick event or other native props.
  • It can’t handle additional classes passed via className without overwriting its internal styles.
  • It doesn’t support working with refs (e.g., for focusing on the button imperatively).
  • It’s not polymorphic—you can’t render other elements (like <a> or <div>) with the same styles.

Optimizing the Code

The minimal code required to centralize button styles, keep semantic usage, and avoid duplication is:

<code>const sizeStyles = {
  sm: 'px-3 py-1 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-4 text-lg',
};

const intentStyles = {
  primary: 'bg-blue-600 text-blue-50',
  secondary: 'bg-orange-600 text-orange-50',
  default: 'text-slate-600 text-slate-50',
};

export const button = ({ size, intent }: { size: keyof typeof sizeStyles; intent: keyof typeof intentStyles }) =>
  `inline-block border-0 ${sizeStyles[size]} ${intentStyles[intent]}`;
</code>Code language: JavaScript (javascript)

To use it:

<code>className={button({ intent: 'primary', size: 'lg' })}
</code>Code language: HTML, XML (xml)

This approach avoids prop-drilling issues, is framework-agnostic, and can work in Vue, Svelte, or any framework.


Accelerating Development with CVA

CVA (class-variance-authority) is a JavaScript library for building CSS components based on variants. Here’s how you can use it to solve the button example:

<code>import { cva } from 'class-variance-authority';

export const button = cva('inline-block border-0', {
  variants: {
    size: {
      sm: 'px-3 py-1 text-sm',
      md: 'px-4 py-2 text-base',
      lg: 'px-6 py-4 text-lg',
    },
    intent: {
      primary: 'bg-blue-600 text-blue-50',
      secondary: 'bg-orange-600 text-orange-50',
      default: 'text-slate-600 text-slate-50',
    },
  },
});
</code>Code language: JavaScript (javascript)

To invoke:

<code>className={button({ intent: 'primary', size: 'lg' })}
</code>Code language: HTML, XML (xml)

Extending Component Classes

To add additional classes to the component, use the class property (or className for React):

<code>className={button({
  intent: 'primary',
  size: 'lg',
  class: 'text-blue-600',
})}
</code>Code language: JavaScript (javascript)

Extracting Types

You can extract types for component variants using VariantProps:

<code>import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';

export const button = cva(/* ... */);

export type ButtonProps = VariantProps<typeof button>;
</code>Code language: JavaScript (javascript)

Default Variants

Set default variants with defaultVariants:

<code>export const button = cva('inline-block border-0', {
  variants: {
    size: { sm: 'px-3 py-1 text-sm', md: 'px-4 py-2 text-base', lg: 'px-6 py-4 text-lg' },
    intent: { primary: 'bg-blue-600 text-blue-50', secondary: 'bg-orange-600 text-orange-50', default: 'text-slate-600 text-slate-50' },
  },
  defaultVariants: { size: 'md', intent: 'default' },
});
</code>Code language: JavaScript (javascript)

Compound Variants

To define styles for multiple conditions:

<code>export const button = cva('inline-block border-0', {
  variants: {
    size: { sm: 'px-3 py-1 text-sm border', md: 'px-4 py-2 text-base border-2', lg: 'px-6 py-4 text-lg border-4' },
    outline: { false: 'border-transparent', true: null },
    intent: { primary: null, secondary: null, default: null },
  },
  compoundVariants: [
    { intent: 'primary', outline: false, class: 'bg-blue-600 text-blue-50' },
    { intent: 'primary', outline: true, class: 'bg-blue-50 text-blue-600 border-blue-600' },
  ],
  defaultVariants: { size: 'md', intent: 'default', outline: false },
});
</code>Code language: JavaScript (javascript)

Avoiding Class Collisions with Tailwind Merge

Use tailwind-merge to resolve class conflicts:

<code>import { twMerge } from 'tailwind-merge';

twMerge('p-3', 'p-4'); // Output: p-4
twMerge('block', 'flex'); // Output: flex
twMerge('p-3 hover:p-4', 'p-2'); // Output: p-2 hover:p-4
</code>Code language: JavaScript (javascript)

Conclusion

Building a UI component that meets functional requirements is not trivial, especially for purely presentational components. In such cases, CVA offers a practical and efficient alternative, with the added benefit of being framework-agnostic.

Related Posts

SolidJS, Javascript, Frameworks

Web Animation: How to Create Engaging and Interactive User Experiences

Grace Lau
September 1, 2023
optifine

OptiFine for Minecraft? You Should Definitely Try It

Lucilla Tomassi
August 25, 2023
react component librararies for UI design.

React Component Libraries for Boosting Your UI Design

Codemotion
July 6, 2023
deceptive design, ux

Deceptive Patterns In UX Design – How To Avoid Them?

Codemotion
January 27, 2023
Share on:facebooktwitterlinkedinreddit

Tagged as:Careers CSS Tailwind

jmlweb
Frontend Solutions Architect @ Freepik
Technological Innovations of 2025
Previous Post
DeepSeek: Coding Assistant Making Waves in AI
Next Post

Footer

Discover

  • Events
  • Community
  • Partners
  • Become a partner
  • Hackathons

Magazine

  • Tech articles

Talent

  • Discover talent
  • Jobs

Companies

  • Discover companies

For Business

  • Codemotion for companies

About

  • About us
  • Become a contributor
  • Work with us
  • Contact us

Follow Us

© Copyright Codemotion srl Via Marsala, 29/H, 00185 Roma P.IVA 12392791005 | Privacy policy | Terms and conditions