@@ -737,11 +737,32 @@ export function rewriteJsxStep(ctx: TransformContext): StepResult {
737737 styleAttr = null ;
738738 }
739739
740+ // Build extra className expression from CSS module classes (if any).
741+ const extraClassNameExpr =
742+ decl . extraClassNames && decl . extraClassNames . length > 0
743+ ? buildExtraClassNameExpr ( j , decl . extraClassNames )
744+ : undefined ;
745+
740746 // Build final rest with stylex.props inserted after last spread.
741747 // For inlined components with className/style, use adapter-configured
742748 // merger behavior (or verbose fallback when no merger is configured).
743- const needsMerge = classNameAttr !== null || styleAttr !== null ;
744749 const isIntrinsicTag = / ^ [ a - z ] / . test ( finalTag ) && ! finalTag . includes ( "." ) ;
750+
751+ // When NOT using sx prop, CSS module classNames must be merged into
752+ // the stylex.props spread (via classNameAttr) to avoid a duplicate
753+ // className attribute that would override the spread's className.
754+ // When using sx prop, sx and className are independent attributes.
755+ let effectiveClassNameAttr = classNameAttr ;
756+ if ( extraClassNameExpr && ! ctx . adapter . useSxProp ) {
757+ // Synthesize a JSX className attribute so buildInlineMergeCall
758+ // folds the CSS module class into the spread merge.
759+ effectiveClassNameAttr = j . jsxAttribute (
760+ j . jsxIdentifier ( "className" ) ,
761+ j . jsxExpressionContainer ( extraClassNameExpr ) ,
762+ ) ;
763+ }
764+
765+ const needsMerge = effectiveClassNameAttr !== null || styleAttr !== null ;
745766 const useSxProp = ctx . adapter . useSxProp && ! needsMerge && isIntrinsicTag ;
746767 const stylexAttr = useSxProp
747768 ? ( ( ) => {
@@ -756,7 +777,7 @@ export function rewriteJsxStep(ctx: TransformContext): StepResult {
756777 ? buildInlineMergeCall (
757778 j ,
758779 styleArgs ,
759- classNameAttr ,
780+ effectiveClassNameAttr ,
760781 styleAttr ,
761782 ctx . adapter . styleMerger ?. functionName ,
762783 )
@@ -765,25 +786,16 @@ export function rewriteJsxStep(ctx: TransformContext): StepResult {
765786 [ ...styleArgs ] ,
766787 ) ,
767788 ) ;
768- // Emit extraClassNames as a className attribute (CSS module classes)
789+
790+ // For sx prop mode, emit extraClassNames as a separate className attribute
791+ // (sx and className are independent and don't conflict).
769792 const extraClassNameAttrs : typeof keptRestAfterVariants = [ ] ;
770- if ( decl . extraClassNames && decl . extraClassNames . length > 0 ) {
771- const classNameExprs = decl . extraClassNames . map ( ( cn ) => cn . expr ) ;
772- const classNameExpr =
773- classNameExprs . length === 1 && classNameExprs [ 0 ]
774- ? classNameExprs [ 0 ]
775- : ( ( ) => {
776- // Multiple: join with template literal `${a} ${b}`
777- const qs : ReturnType < typeof j . templateElement > [ ] = [ ] ;
778- for ( let i = 0 ; i <= classNameExprs . length ; i ++ ) {
779- const isLast = i === classNameExprs . length ;
780- const raw = i === 0 || isLast ? "" : " " ;
781- qs . push ( j . templateElement ( { raw, cooked : raw } , isLast ) ) ;
782- }
783- return j . templateLiteral ( qs , classNameExprs ) ;
784- } ) ( ) ;
793+ if ( extraClassNameExpr && useSxProp ) {
785794 extraClassNameAttrs . push (
786- j . jsxAttribute ( j . jsxIdentifier ( "className" ) , j . jsxExpressionContainer ( classNameExpr ) ) ,
795+ j . jsxAttribute (
796+ j . jsxIdentifier ( "className" ) ,
797+ j . jsxExpressionContainer ( extraClassNameExpr ) ,
798+ ) ,
787799 ) ;
788800 }
789801
@@ -935,6 +947,28 @@ function extractJsxAttrValueExpr(
935947 return undefined ;
936948}
937949
950+ /**
951+ * Builds a single expression from extra className entries (CSS module classes).
952+ * Single entry: returns the expression directly.
953+ * Multiple entries: joins with a template literal `${a} ${b}`.
954+ */
955+ function buildExtraClassNameExpr (
956+ j : TransformContext [ "j" ] [ "jscodeshift" ] ,
957+ extraClassNames : NonNullable < StyledDecl [ "extraClassNames" ] > ,
958+ ) : ExpressionKind {
959+ const exprs = extraClassNames . map ( ( cn ) => cn . expr ) ;
960+ if ( exprs . length === 1 && exprs [ 0 ] ) {
961+ return exprs [ 0 ] ;
962+ }
963+ const qs : ReturnType < typeof j . templateElement > [ ] = [ ] ;
964+ for ( let i = 0 ; i <= exprs . length ; i ++ ) {
965+ const isLast = i === exprs . length ;
966+ const raw = i === 0 || isLast ? "" : " " ;
967+ qs . push ( j . templateElement ( { raw, cooked : raw } , isLast ) ) ;
968+ }
969+ return j . templateLiteral ( qs , exprs ) ;
970+ }
971+
938972/**
939973 * Finds the combined style key matching the consumed props at a JSX call site.
940974 * Returns the style key if a matching combination exists, or undefined otherwise.
0 commit comments