diff --git a/.changeset/fix-css-layer-name-update-props.md b/.changeset/fix-css-layer-name-update-props.md new file mode 100644 index 00000000000..5961b5f6ea3 --- /dev/null +++ b/.changeset/fix-css-layer-name-update-props.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +Fix `cssLayerName` from theme not being applied after initial mount. When `ClerkProvider` re-renders, the `updateProps` handler overwrote the extracted `cssLayerName` with the raw appearance object, causing Clerk's runtime CSS to not be wrapped in `@layer`. This broke Tailwind utility overrides when using themes like `shadcn` that set `cssLayerName: 'components'`. diff --git a/packages/ui/src/Components.tsx b/packages/ui/src/Components.tsx index 9b290c5ca9a..42c4db01fc6 100644 --- a/packages/ui/src/Components.tsx +++ b/packages/ui/src/Components.tsx @@ -397,6 +397,11 @@ const Components = (props: ComponentsProps) => { } } + // Extract cssLayerName from theme if present and move it to appearance level + if (restProps.appearance) { + restProps = { ...restProps, appearance: extractCssLayerNameFromAppearance(restProps.appearance) }; + } + setState(s => ({ ...s, ...restProps, options: { ...s.options, ...restProps.options } })); }; diff --git a/packages/ui/src/utils/__tests__/extractCssLayerNameFromAppearance.test.ts b/packages/ui/src/utils/__tests__/extractCssLayerNameFromAppearance.test.ts new file mode 100644 index 00000000000..93146b3cd38 --- /dev/null +++ b/packages/ui/src/utils/__tests__/extractCssLayerNameFromAppearance.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; + +import { extractCssLayerNameFromAppearance } from '../extractCssLayerNameFromAppearance'; + +describe('extractCssLayerNameFromAppearance', () => { + it('promotes cssLayerName from a single theme to the appearance level', () => { + const theme = { + name: 'shadcn', + cssLayerName: 'components', + variables: {}, + elements: {}, + __type: 'prebuilt_appearance' as const, + }; + + const result = extractCssLayerNameFromAppearance({ theme }); + + expect(result?.cssLayerName).toBe('components'); + }); + + it('promotes cssLayerName from a theme array to the appearance level', () => { + const theme = { + name: 'shadcn', + cssLayerName: 'components', + variables: {}, + elements: {}, + __type: 'prebuilt_appearance' as const, + }; + + const result = extractCssLayerNameFromAppearance({ theme: [theme] }); + + expect(result?.cssLayerName).toBe('components'); + }); + + it('preserves explicit cssLayerName on appearance over theme cssLayerName', () => { + const theme = { + name: 'shadcn', + cssLayerName: 'components', + variables: {}, + elements: {}, + __type: 'prebuilt_appearance' as const, + }; + + const result = extractCssLayerNameFromAppearance({ theme, cssLayerName: 'custom' }); + + expect(result?.cssLayerName).toBe('custom'); + }); + + it('returns appearance unchanged when no theme is present', () => { + const appearance = { cssLayerName: 'custom' }; + const result = extractCssLayerNameFromAppearance(appearance); + + expect(result).toEqual(appearance); + }); + + it('returns undefined for undefined input', () => { + expect(extractCssLayerNameFromAppearance(undefined)).toBeUndefined(); + }); + + it('persists cssLayerName when appearance is re-extracted after an updateProps-style state merge', () => { + const theme = { + name: 'shadcn', + cssLayerName: 'components', + variables: {}, + elements: {}, + __type: 'prebuilt_appearance' as const, + }; + + // Initial mount: cssLayerName is extracted to appearance level + const initialAppearance = extractCssLayerNameFromAppearance({ theme }); + expect(initialAppearance?.cssLayerName).toBe('components'); + + // ClerkProvider re-renders and sends raw appearance (cssLayerName only inside theme) + const rawAppearance = { theme }; + + // updateProps must re-extract before merging into state + const reExtracted = extractCssLayerNameFromAppearance(rawAppearance); + expect(reExtracted?.cssLayerName).toBe('components'); + }); + + it('respects a new cssLayerName passed via updateProps', () => { + const theme = { + name: 'shadcn', + cssLayerName: 'components', + variables: {}, + elements: {}, + __type: 'prebuilt_appearance' as const, + }; + + // User changes cssLayerName at the appearance level + const updatedAppearance = { theme, cssLayerName: 'utilities' }; + const result = extractCssLayerNameFromAppearance(updatedAppearance); + + // Explicit appearance-level cssLayerName takes precedence over theme + expect(result?.cssLayerName).toBe('utilities'); + }); +});