Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-css-layer-name-update-props.md
Original file line number Diff line number Diff line change
@@ -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'`.
5 changes: 5 additions & 0 deletions packages/ui/src/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }));
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
Loading