Skip to content

Conversation

@jonsherrard
Copy link

@jonsherrard jonsherrard commented Jan 21, 2026

Note this is mostly ai-generated PR and i will be testing over next few days with a couple of real-world react native apps to see how it handles and fix any issues.

feat: Add compile-time support for Tailwind Variants, CVA, and tailwind-merge

Summary

This PR adds compile-time Babel transformation support for the three major Tailwind CSS class utility libraries with all their key exports:

Library Exports Purpose
tailwind-variants tv Variant function creator
class-variance-authority cva, cx Variant creator + class concatenation
tailwind-merge twMerge, twJoin Merge with/without conflict resolution

Components using these libraries work unchanged on web and are automatically transformed to optimized StyleSheet.create references for React Native.

Implementation Highlights

Config-Driven Import Tracking

Adding new libraries or exports is just one line in the config:

// src/babel/plugin/state.ts
export const CLASS_UTILITY_CONFIG: Record<string, Record<string, ClassUtilityType>> = {
  "tailwind-variants": {
    tv: "tv",
  },
  "class-variance-authority": {
    cva: "cva",
    cx: "cx",      // Re-export of clsx for class concatenation
  },
  "tailwind-merge": {
    twMerge: "twMerge",  // Merge with conflict resolution
    twJoin: "twJoin",    // Join without conflict resolution (faster)
  },
};

Unified Processing

All class joiner utilities (twMerge, twJoin, cx) share processing logic:

const CLASS_JOINERS = {
  twMerge: (...classes) => twMerge(...classes),  // Conflicts resolved
  twJoin: (...classes) => twJoin(...classes),    // No conflict resolution
  cx: (...classes) => cx(...classes),            // Simple concatenation
};

How Each Utility is Processed

1. Variant Functions (tv, cva)

These create variant functions called later. We pre-compute all combinations:

// Input
const button = tv({
  base: 'font-semibold rounded-lg',
  variants: {
    color: { primary: 'bg-blue-500', secondary: 'bg-gray-500' },
  },
});

<Button className={button({ color: 'primary' })} />

// Output
const _twStyles = StyleSheet.create({
  _button_color_primary: { fontWeight: '600', borderRadius: 8, backgroundColor: '#3b82f6' },
});

<Button style={_twStyles._button_color_primary} />

2. Class Joiners (twMerge, twJoin, cx)

These are called directly. We resolve at compile time using the actual library:

// Input
import { twMerge } from 'tailwind-merge';
<div className={twMerge('px-2 py-1', 'p-4')} />

// Output (twMerge resolves px-2 py-1 + p-4 = p-4)
const _twStyles = StyleSheet.create({
  _p4: { padding: 16 },
});

<div style={_twStyles._p4} />

Features Supported

Tailwind Variants (tv)

  • Base classes, variants, default variants, compound variants
  • Boolean variants (true/false keys)
  • Aliased imports
  • Slots (warning logged, falls back to runtime)

Class Variance Authority (cva, cx)

  • cva: Full variant support (base, variants, compounds, defaults)
  • cx: Class concatenation (alias for clsx)
  • Aliased imports

tailwind-merge (twMerge, twJoin)

  • twMerge: Merge with conflict resolution (later classes win)
  • twJoin: Join without conflict resolution (faster)
  • Multiple string arguments, static template literals
  • Aliased imports

Edge Cases

  • Multiple imports from same library (import { cva, cx })
  • All libraries in same file
  • Dynamic configs/arguments preserved for runtime

Files Changed

New Files

  • src/babel/utils/variantProcessing.ts - Core processing utilities
  • src/babel/plugin/visitors/variants.ts - Babel visitors
  • src/babel/plugin/visitors/variants.test.ts - 28 comprehensive tests

Modified Files

  • src/babel/plugin.ts - Integrated class utility visitors
  • src/babel/plugin/state.ts - Added unified tracking types and config
  • src/babel/plugin/visitors/program.ts - Added cleanup on program exit

Dependencies Added

{
  "dependencies": {
    "class-variance-authority": "0.7.1",
    "tailwind-variants": "3.2.2",
    "tailwind-merge": "3.4.0"
  }
}

Test Results

Test Files  28 passed (28)
     Tests  1060 passed | 1 skipped (1061)
  • All 1032 existing tests continue to pass
  • 28 new tests for class utility transformation

Example: Using All Libraries

// Input - works on web with standard Tailwind CSS
import { tv } from 'tailwind-variants';
import { cva, cx } from 'class-variance-authority';
import { twMerge, twJoin } from 'tailwind-merge';

const tvButton = tv({
  base: 'rounded',
  variants: { size: { sm: 'p-1' } },
});

const cvaButton = cva('font-bold', {
  variants: { color: { red: 'text-red-500' } },
});

function Component() {
  return (
    <div>
      <button className={tvButton({ size: 'sm' })} />
      <button className={cvaButton({ color: 'red' })} />
      <span className={cx('text-sm', 'font-bold')} />
      <span className={twMerge('px-2', 'px-4')} />
      <span className={twJoin('py-1', 'py-2')} />
    </div>
  );
}
// Output - optimized for React Native
import { StyleSheet } from 'react-native';

const _twStyles = StyleSheet.create({
  _tvButton_size_sm: { borderRadius: 4, padding: 4 },
  _cvaButton_color_red: { fontWeight: '700', color: '#ef4444' },
  _text_sm_font_bold: { fontSize: 14, fontWeight: '700' },
  _px4: { paddingHorizontal: 16 },  // twMerge resolved conflict
  _py1_py2: { paddingVertical: 4, paddingVertical: 8 },  // twJoin kept both
});

function Component() {
  return (
    <div>
      <button style={_twStyles._tvButton_size_sm} />
      <button style={_twStyles._cvaButton_color_red} />
      <span style={_twStyles._text_sm_font_bold} />
      <span style={_twStyles._px4} />
      <span style={_twStyles._py1_py2} />
    </div>
  );
}

Breaking Changes

None. Purely additive feature.

Future Improvements

  • Full slots support for tv()
  • Support for custom cn() utility patterns
  • Runtime fallback for complex dynamic cases

Checklist

  • Tests pass (npm test)
  • Build passes (npm run build)
  • Lint passes (npm run lint)
  • Type checking passes (npm run check)
  • No breaking changes
  • Comprehensive test coverage

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant