Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .changeset/yummy-plants-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Refactor base theme approach to enable opting into simple theme.

```tsx
appearance={{
theme: 'simple' // removes Clerk base theme
}}
```
2 changes: 2 additions & 0 deletions packages/clerk-js/sandbox/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ function appearanceVariableOptions() {
const updateVariables = () => {
void Clerk.__unstable__updateProps({
appearance: {
// Preserve existing appearance properties like baseTheme
...Clerk.__internal_getOption('appearance'),
variables: Object.fromEntries(
Object.entries(variableInputs).map(([key, input]) => {
sessionStorage.setItem(key, input.value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const inputStyles = (theme: InternalTheme) => ({
}),
});

export const polishedAppearance: Appearance = {
const clerkTheme: Appearance = {
elements: ({ theme }: { theme: InternalTheme }): Elements => {
return {
button: {
Expand Down Expand Up @@ -266,3 +266,21 @@ export const polishedAppearance: Appearance = {
};
},
} satisfies Appearance;

const simpleTheme: Appearance = {
// @ts-expect-error Internal API for simple theme detection
simpleStyles: true,
elements: {},
} satisfies Appearance;

export const getBaseTheme = (theme: 'clerk' | 'simple' = 'clerk'): Appearance => {
switch (theme) {
case 'simple':
return simpleTheme;
case 'clerk':
default:
return clerkTheme;
}
};

export const baseTheme = clerkTheme;
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
label={localizationKeys('organizationSwitcher.action__manageOrganization')}
onClick={() => handleItemClick()}
trailing={<NotificationCountBadgeManageButton />}
focusRing
/>
);

Expand All @@ -125,6 +126,7 @@ export const OrganizationSwitcherPopover = React.forwardRef<HTMLDivElement, Orga
icon={Billing}
label={runIfFunctionOrReturn(__unstable_manageBillingLabel) || 'Upgrade'}
onClick={() => router.navigate(runIfFunctionOrReturn(__unstable_manageBillingUrl))}
focusRing
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,7 @@ function Card(props: CardProps) {
background: common.mutedBackground(t),
borderWidth: t.borderWidths.$normal,
borderStyle: t.borderStyles.$solid,
borderColor: t.colors.$borderAlpha100,
boxShadow: !isCompact ? t.shadows.$cardBoxShadow : t.shadows.$tableBodyShadow,
borderColor: t.colors.$borderAlpha150,
borderRadius: t.radii.$xl,
overflow: 'hidden',
textAlign: 'left',
Expand Down Expand Up @@ -205,7 +204,7 @@ function Card(props: CardProps) {
backgroundColor: hasFeatures ? t.colors.$colorBackground : 'transparent',
borderTopWidth: hasFeatures ? t.borderWidths.$normal : 0,
borderTopStyle: t.borderStyles.$solid,
borderTopColor: t.colors.$borderAlpha100,
borderTopColor: t.colors.$borderAlpha150,
})}
data-variant={isCompact ? 'compact' : 'default'}
>
Expand All @@ -225,7 +224,7 @@ function Card(props: CardProps) {
padding: isCompact ? t.space.$3 : t.space.$4,
borderTopWidth: t.borderWidths.$normal,
borderTopStyle: t.borderStyles.$solid,
borderTopColor: t.colors.$borderAlpha100,
borderTopColor: t.colors.$borderAlpha150,
order: ctaPosition === 'top' ? -1 : undefined,
})}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export const MultiSessionActions = (props: MultiSessionActionsProps) => {
icon={CogFilled}
label={localizationKeys('userButton.action__manageAccount')}
onClick={handleManageAccountClicked}
focusRing
/>
<SmallAction
elementDescriptor={descriptors.userButtonPopoverActionButton}
Expand All @@ -193,6 +194,7 @@ export const MultiSessionActions = (props: MultiSessionActionsProps) => {
icon={SignOut}
label={localizationKeys('userButton.action__signOut')}
onClick={handleSignOutSessionClicked(session)}
focusRing
/>
</Flex>
</SmallActions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ describe('AppearanceProvider layout flows', () => {
expect(result.current.parsedLayout.socialButtonsVariant).toBe('blockButton');
});

it('removes the polishedAppearance when simpleStyles is passed to globalAppearance', () => {
it('removes the baseTheme when simpleStyles is passed to globalAppearance', () => {
const wrapper = ({ children }) => (
<AppearanceProvider
appearanceKey='signIn'
Expand All @@ -359,7 +359,7 @@ describe('AppearanceProvider layout flows', () => {
expect(result.current.parsedElements[0]['alert'].backgroundColor).toBe(themeAColor);
});

it('removes the polishedAppearance when simpleStyles is passed to appearance', () => {
it('removes the baseTheme when simpleStyles is passed to appearance', () => {
const wrapper = ({ children }) => (
<AppearanceProvider
appearanceKey='signIn'
Expand Down Expand Up @@ -464,3 +464,124 @@ describe('AppearanceProvider captcha', () => {
expect(result.current.parsedCaptcha.language).toBe('');
});
});

describe('AppearanceProvider theme flows', () => {
it('supports string-based theme property with "clerk" value', () => {
const wrapper = ({ children }) => (
<AppearanceProvider
appearanceKey='signIn'
appearance={{
theme: 'clerk',
}}
>
{children}
</AppearanceProvider>
);

const { result } = renderHook(() => useAppearance(), { wrapper });
// Should include clerk theme styles (baseTheme will be included)
expect(result.current.parsedElements.length).toBeGreaterThan(0);
});

it('supports string-based theme property with "simple" value', () => {
const wrapper = ({ children }) => (
<AppearanceProvider
appearanceKey='signIn'
appearance={{
theme: 'simple',
}}
>
{children}
</AppearanceProvider>
);

const { result } = renderHook(() => useAppearance(), { wrapper });
// Should include both simple theme and base theme (2 elements total)
expect(result.current.parsedElements.length).toBe(2);
});

it('theme property takes precedence over deprecated baseTheme', () => {
const wrapper = ({ children }) => (
<AppearanceProvider
appearanceKey='signIn'
appearance={{
theme: 'simple',
baseTheme: 'clerk', // This should be ignored
}}
>
{children}
</AppearanceProvider>
);

const { result } = renderHook(() => useAppearance(), { wrapper });
// Should include both simple theme and base theme (2 elements total)
expect(result.current.parsedElements.length).toBe(2);
});

it('maintains backward compatibility with baseTheme property', () => {
const wrapper = ({ children }) => (
<AppearanceProvider
appearanceKey='signIn'
appearance={{
baseTheme: 'simple',
}}
>
{children}
</AppearanceProvider>
);

const { result } = renderHook(() => useAppearance(), { wrapper });
// Should work the same as theme: 'simple' (2 elements total)
expect(result.current.parsedElements.length).toBe(2);
});

it('supports object-based themes with new theme property', () => {
const customTheme = {
elements: {
card: { backgroundColor: 'red' },
},
};

const wrapper = ({ children }) => (
<AppearanceProvider
appearanceKey='signIn'
appearance={{
theme: customTheme,
}}
>
{children}
</AppearanceProvider>
);

const { result } = renderHook(() => useAppearance(), { wrapper });
// Should include base theme + custom theme
expect(result.current.parsedElements.length).toBeGreaterThan(1);
expect(result.current.parsedElements.some(el => el.card?.backgroundColor === 'red')).toBe(true);
});

it('supports array-based themes with new theme property', () => {
const themeA = {
elements: { card: { backgroundColor: 'red' } },
};
const themeB = {
elements: { card: { color: 'blue' } },
};

const wrapper = ({ children }) => (
<AppearanceProvider
appearanceKey='signIn'
appearance={{
theme: [themeA, themeB],
}}
>
{children}
</AppearanceProvider>
);

const { result } = renderHook(() => useAppearance(), { wrapper });
// Should include base theme + both custom themes
expect(result.current.parsedElements.length).toBeGreaterThan(2);
expect(result.current.parsedElements.some(el => el.card?.backgroundColor === 'red')).toBe(true);
expect(result.current.parsedElements.some(el => el.card?.color === 'blue')).toBe(true);
});
});
34 changes: 27 additions & 7 deletions packages/clerk-js/src/ui/customizables/parseAppearance.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { fastDeepMergeAndReplace } from '@clerk/shared/utils';
import type { Appearance, CaptchaAppearanceOptions, DeepPartial, Elements, Layout, Theme } from '@clerk/types';

import { baseTheme, getBaseTheme } from '../baseTheme';
import { createInternalTheme, defaultInternalTheme } from '../foundations';
import { polishedAppearance } from '../polishedAppearance';
import type { InternalTheme } from '../styledSystem';
import {
createColorScales,
Expand All @@ -21,7 +21,7 @@ export type ParsedCaptcha = Required<CaptchaAppearanceOptions>;

type PublicAppearanceTopLevelKey = keyof Omit<
Appearance,
'baseTheme' | 'elements' | 'layout' | 'variables' | 'captcha' | 'cssLayerName'
'baseTheme' | 'theme' | 'elements' | 'layout' | 'variables' | 'captcha' | 'cssLayerName'
>;

export type AppearanceCascade = {
Expand Down Expand Up @@ -83,7 +83,7 @@ export const parseAppearance = (cascade: AppearanceCascade): ParsedAppearance =>
return !!a.simpleStyles;
})
) {
appearanceList.unshift(polishedAppearance);
appearanceList.unshift(baseTheme);
}

const parsedElements = parseElements(
Expand All @@ -104,9 +104,18 @@ const expand = (theme: Theme | undefined, cascade: any[]) => {
return;
}

(Array.isArray(theme.baseTheme) ? theme.baseTheme : [theme.baseTheme]).forEach(baseTheme =>
expand(baseTheme as Theme, cascade),
);
// Use new 'theme' property if available, otherwise fall back to deprecated 'baseTheme'
const themeProperty = theme.theme !== undefined ? theme.theme : theme.baseTheme;

if (themeProperty !== undefined) {
(Array.isArray(themeProperty) ? themeProperty : [themeProperty]).forEach(baseTheme => {
if (typeof baseTheme === 'string') {
expand(getBaseTheme(baseTheme), cascade);
} else {
expand(baseTheme as Theme, cascade);
}
});
}

cascade.push(theme);
};
Expand All @@ -122,7 +131,18 @@ const parseLayout = (appearanceList: Appearance[]) => {
const parseCaptcha = (appearanceList: Appearance[]) => {
return {
...defaultCaptchaOptions,
...appearanceList.reduce((acc, appearance) => ({ ...acc, ...appearance.captcha }), {}),
...appearanceList.reduce((acc, appearance) => {
if (appearance.captcha) {
const { theme: captchaTheme, size, language } = appearance.captcha;
return {
...acc,
...(captchaTheme && { theme: captchaTheme }),
...(size && { size }),
...(language && { language }),
};
}
return acc;
}, {} as Partial<CaptchaAppearanceOptions>),
};
};

Expand Down
3 changes: 1 addition & 2 deletions packages/clerk-js/src/ui/elements/Action/ActionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@ export const ActionCard = (props: ActionCardProps) => {
elementDescriptor={descriptors.actionCard}
sx={[
t => ({
boxShadow: t.shadows.$actionCardShadow,
gap: t.space.$4,
borderRadius: t.radii.$lg,
padding: `${t.space.$4} ${t.space.$5}`,
borderWidth: t.borderWidths.$normal,
borderStyle: t.borderStyles.$solid,
borderColor: t.colors.$borderAlpha100,
borderColor: t.colors.$borderAlpha150,
...styles(t)[variant],
}),
sx,
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/elements/Card/CardContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ export const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>((p
zIndex: t.zIndices.$card,
borderWidth: t.borderWidths.$normal,
borderStyle: t.borderStyles.$solid,
borderColor: t.colors.$borderAlpha50,
boxShadow: t.shadows.$cardContentShadow,
borderColor: t.colors.$borderAlpha150,
borderRadius: t.radii.$lg,
position: 'relative',
padding: `${t.space.$8} ${t.space.$10}`,
justifyContent: 'center',
alignContent: 'center',
marginBlockStart: '-1px',
marginInline: '-1px',
}),
sx,
]}
Expand Down
3 changes: 1 addition & 2 deletions packages/clerk-js/src/ui/elements/Card/CardRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ export const CardRoot = React.forwardRef<HTMLDivElement, CardRootProps>((props,
width: t.sizes.$100,
borderWidth: t.borderWidths.$normal,
borderStyle: t.borderStyles.$solid,
borderColor: t.colors.$borderAlpha100,
boxShadow: t.shadows.$cardBoxShadow,
borderColor: t.colors.$borderAlpha150,
borderRadius: t.radii.$xl,
color: t.colors.$colorForeground,
position: 'relative',
Expand Down
5 changes: 2 additions & 3 deletions packages/clerk-js/src/ui/elements/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,7 @@ const Content = React.forwardRef<HTMLDivElement, ContentProps>(({ children }, re
borderStartEndRadius: strategy === 'fixed' ? t.radii.$lg : 0,
borderWidth: t.borderWidths.$normal,
borderStyle: t.borderStyles.$solid,
borderColor: t.colors.$borderAlpha100,
boxShadow: t.shadows.$cardBoxShadow,
borderColor: t.colors.$borderAlpha150,
overflow: 'hidden',
pointerEvents: 'auto',
})}
Expand Down Expand Up @@ -304,7 +303,7 @@ const Header = React.forwardRef<HTMLDivElement, HeaderProps>(({ title, children,
),
borderBlockEndWidth: t.borderWidths.$normal,
borderBlockEndStyle: t.borderStyles.$solid,
borderBlockEndColor: t.colors.$borderAlpha100,
borderBlockEndColor: t.colors.$borderAlpha150,
borderStartStartRadius: t.radii.$lg,
borderStartEndRadius: t.radii.$lg,
paddingBlock: title ? t.space.$3 : undefined,
Expand Down
Loading
Loading