= {};
+ if (initialElevation) {
+ initialAppearanceOptions.elevation = initialElevation;
+ }
+ if (initialDevWarnings === 'off') {
+ initialAppearanceOptions.unsafe_disableDevelopmentModeWarnings = true;
+ }
await Clerk.load({
...(componentControls.clerk.getProps() ?? {}),
@@ -549,6 +595,7 @@ void (async () => {
appearance: {
...(initialTheme ? { theme: initialTheme } : {}),
...presetToAppearance(initialPreset),
+ ...(Object.keys(initialAppearanceOptions).length ? { options: initialAppearanceOptions } : {}),
},
});
renderCurrentRoute();
@@ -560,6 +607,7 @@ void (async () => {
updateVariables();
}
updateOtherOptions();
+ updateAppearanceOptions();
} else {
console.error(`Unknown route: "${route}".`);
}
diff --git a/packages/clerk-js/sandbox/template.html b/packages/clerk-js/sandbox/template.html
index 9591fe7e852..0a15fd55400 100644
--- a/packages/clerk-js/sandbox/template.html
+++ b/packages/clerk-js/sandbox/template.html
@@ -33,7 +33,7 @@
}
-
+
localization
+
+
diff --git a/packages/ui/src/customizables/AppearanceContext.tsx b/packages/ui/src/customizables/AppearanceContext.tsx
index 8bfee6007b9..9d180bd9e77 100644
--- a/packages/ui/src/customizables/AppearanceContext.tsx
+++ b/packages/ui/src/customizables/AppearanceContext.tsx
@@ -23,4 +23,4 @@ const AppearanceProvider = (props: AppearanceProviderProps) => {
return {props.children};
};
-export { AppearanceProvider, useAppearance };
+export { AppearanceContext, AppearanceProvider, useAppearance };
diff --git a/packages/ui/src/customizables/index.ts b/packages/ui/src/customizables/index.ts
index e45ce741652..5b3d141a574 100644
--- a/packages/ui/src/customizables/index.ts
+++ b/packages/ui/src/customizables/index.ts
@@ -6,7 +6,7 @@ import { makeResponsive } from './makeResponsive';
import { sanitizeDomProps } from './sanitizeDomProps';
export * from './Flow';
-export { AppearanceProvider, useAppearance } from './AppearanceContext';
+export { AppearanceContext, AppearanceProvider, useAppearance } from './AppearanceContext';
export { descriptors } from './elementDescriptors';
export { localizationKeys, useLocalizations } from '../localization';
export type { LocalizationKey } from '../localization';
diff --git a/packages/ui/src/customizables/parseAppearance.ts b/packages/ui/src/customizables/parseAppearance.ts
index a61b883191f..fac6df2d01b 100644
--- a/packages/ui/src/customizables/parseAppearance.ts
+++ b/packages/ui/src/customizables/parseAppearance.ts
@@ -51,6 +51,7 @@ const defaultOptions: ParsedOptions = {
shimmer: true,
animations: true,
unsafe_disableDevelopmentModeWarnings: false,
+ elevation: 'raised',
};
const defaultCaptchaOptions: ParsedCaptcha = {
diff --git a/packages/ui/src/elements/Card/CardRoot.tsx b/packages/ui/src/elements/Card/CardRoot.tsx
index 18f1d4b88d9..5c81fe2fc1c 100644
--- a/packages/ui/src/elements/Card/CardRoot.tsx
+++ b/packages/ui/src/elements/Card/CardRoot.tsx
@@ -1,18 +1,100 @@
import React from 'react';
-import { Col, descriptors, generateFlowPartClassname, useAppearance } from '../../customizables';
+import { AppearanceContext, Col, descriptors, generateFlowPartClassname, useAppearance } from '../../customizables';
import type { ElementDescriptor } from '../../customizables/elementDescriptors';
import type { PropsOfComponent } from '../../styledSystem';
import { mqu } from '../../styledSystem';
import { ApplicationLogo } from '../ApplicationLogo';
import { useFlowMetadata } from '../contexts';
+import { ModalContext } from '../Modal';
-type CardRootProps = PropsOfComponent;
+// Element style overrides for flush elevation. Injected into parsedElements at
+// index 1 (after baseTheme, before user overrides) via AppearanceContext so they
+// participate in the makeCustomizable cascade and can still be overridden by users.
+const FLUSH_ELEMENTS = {
+ cardBox: {
+ borderWidth: 0,
+ borderRadius: 0,
+ boxShadow: 'none',
+ overflow: 'visible',
+ },
+ card: {
+ borderWidth: 0,
+ borderRadius: 0,
+ boxShadow: 'none',
+ backgroundColor: 'transparent',
+ padding: 0,
+ marginBlockStart: 0,
+ marginInline: 0,
+ },
+ footer: {
+ background: 'transparent',
+ marginTop: 0,
+ paddingTop: 0,
+ '>:first-of-type': {
+ paddingInline: 0,
+ },
+ '>:not(:first-of-type)': {
+ borderTopWidth: 0,
+ paddingInline: 0,
+ },
+ },
+};
+
+type CardRootProps = PropsOfComponent & {
+ /**
+ * Override the visual elevation for this card instance.
+ * When omitted, falls back to `appearance.options.elevation` for page-mounted
+ * components, and `'raised'` for modals.
+ * Profile and popover card roots pass `'raised'` explicitly to opt out of flush.
+ */
+ elevation?: 'raised' | 'flush';
+};
export const CardRoot = React.forwardRef((props, ref) => {
- const { sx, children, ...rest } = props;
+ const { sx, children, elevation: elevationProp, ...rest } = props;
const appearance = useAppearance();
const flowMetadata = useFlowMetadata();
+ const rawModalCtx = React.useContext(ModalContext);
+ const isModal = rawModalCtx !== undefined;
+ // Explicit prop wins; modals always raised; otherwise use appearance option
+ const elevation = elevationProp ?? (isModal ? 'raised' : appearance.parsedOptions.elevation);
+ const isFlush = elevation === 'flush';
+
+ const augmentedAppearance = React.useMemo(() => {
+ if (!isFlush) {
+ return appearance;
+ }
+ const newParsedElements = [appearance.parsedElements[0], FLUSH_ELEMENTS, ...appearance.parsedElements.slice(1)];
+ return { ...appearance, parsedElements: newParsedElements };
+ }, [appearance, isFlush]);
+
+ const cardBox = (
+ ({
+ isolation: 'isolate',
+ maxWidth: `calc(100vw - ${t.sizes.$10})`,
+ width: t.sizes.$100,
+ borderWidth: t.borderWidths.$normal,
+ borderStyle: t.borderStyles.$solid,
+ borderColor: t.colors.$borderAlpha150,
+ borderRadius: t.radii.$xl,
+ color: t.colors.$colorForeground,
+ position: 'relative',
+ overflow: 'hidden',
+ }),
+ sx,
+ ]}
+ {...rest}
+ >
+ {children}
+
+ );
+
return (
<>
{appearance.parsedOptions.logoPlacement === 'outside' && (
@@ -25,33 +107,11 @@ export const CardRoot = React.forwardRef((props,
})}
/>
)}
- ({
- /**
- * All components should create their own stack context
- * https://developer.mozilla.org/en-US/docs/Web/CSS/isolation
- */
- isolation: 'isolate',
- maxWidth: `calc(100vw - ${t.sizes.$10})`,
- width: t.sizes.$100,
- borderWidth: t.borderWidths.$normal,
- borderStyle: t.borderStyles.$solid,
- borderColor: t.colors.$borderAlpha150,
- borderRadius: t.radii.$xl,
- color: t.colors.$colorForeground,
- position: 'relative',
- overflow: 'hidden',
- }),
- sx,
- ]}
- {...rest}
- >
- {children}
-
+ {isFlush ? (
+ {cardBox}
+ ) : (
+ cardBox
+ )}
>
);
});
diff --git a/packages/ui/src/elements/PopoverCard.tsx b/packages/ui/src/elements/PopoverCard.tsx
index 92ac88cf2a7..f46f1431998 100644
--- a/packages/ui/src/elements/PopoverCard.tsx
+++ b/packages/ui/src/elements/PopoverCard.tsx
@@ -28,6 +28,8 @@ const PopoverCardRoot = React.forwardRef<
elementDescriptor={[descriptors.popoverBox, elementDescriptor as ElementDescriptor]}
{...rest}
ref={ref}
+ // Popover cards always render as raised — flush is scoped to simple card components
+ elevation='raised'
sx={[
t => ({
width: t.sizes.$94,
diff --git a/packages/ui/src/elements/ProfileCard/ProfileCardRoot.tsx b/packages/ui/src/elements/ProfileCard/ProfileCardRoot.tsx
index d700b46446f..42573898182 100644
--- a/packages/ui/src/elements/ProfileCard/ProfileCardRoot.tsx
+++ b/packages/ui/src/elements/ProfileCard/ProfileCardRoot.tsx
@@ -15,6 +15,8 @@ export const ProfileCardRoot = React.forwardRef ({
width: t.sizes.$220,
diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts
index 5cc35d313be..0d73f19ba19 100644
--- a/packages/ui/src/internal/appearance.ts
+++ b/packages/ui/src/internal/appearance.ts
@@ -985,6 +985,26 @@ export type Options = {
* @default false
*/
unsafe_disableDevelopmentModeWarnings?: boolean;
+
+ /**
+ * Controls the visual elevation of card-based components.
+ *
+ * - `'raised'` (default) — the card renders with its border, box-shadow, border-radius, and padding.
+ * - `'flush'` — removes the card border, box-shadow, border-radius, outer padding, and footer
+ * background so the component sits flat against its container.
+ *
+ * Applies to all card-based components including ``, ``,
+ * ``, ``, ``,
+ * ``, ``, and session task components.
+ *
+ * Does **not** affect profile components (``, ``)
+ * or popover components (``, ``), which always render as raised.
+ *
+ * When a component is opened as a modal, it always renders as raised regardless of this setting.
+ *
+ * @default 'raised'
+ */
+ elevation?: 'raised' | 'flush';
};
export type CaptchaAppearanceOptions = {