Repository: clerk/javascript
Pull request #5824: chore(clerk-js,types,localizations): New layout for PricingTable plan cards
Author: login: aeliox
Labels: name: types
name: clerk-js
name: localizations
name: integration
name: testing
Created: 2025-05-04T22:24:48Z | Merged: 2025-05-05T15:50:41Z

## Description

New, better-aligned layouts for plan cards within the `PricingTable`.

<img width="825" alt="Screenshot 2025-05-04 at 5 13 10 PM" src="https://github.com/user-attachments/assets/ce2d789d-d9f2-4903-8f6b-bd595f6199bf" />
<img width="1276" alt="Screenshot 2025-05-04 at 5 12 36 PM" src="https://github.com/user-attachments/assets/d371355d-c29c-4087-b55d-2d8a7e662eb4" />


## Checklist

- [ ] `pnpm test` runs as expected.
- [ ] `pnpm build` runs as expected.
- [ ] (If applicable) [JSDoc comments](https://jsdoc.app/about-getting-started.html) have been added or updated for any package exports
- [ ] (If applicable) [Documentation](https://github.com/clerk/clerk-docs) has been updated

## Type of change

- [ ] 🐛 Bug fix
- [ ] 🌟 New feature
- [ ] 🔨 Breaking change
- [ ] 📖 Refactoring / dependency upgrade / documentation
- [ ] other:



Diff:
diff --git a/.changeset/old-cobras-change.md b/.changeset/old-cobras-change.md
new file mode 100644
index 00000000000..5c5bfd49239
--- /dev/null
+++ b/.changeset/old-cobras-change.md
@@ -0,0 +1,7 @@
+---
+'@clerk/localizations': patch
+'@clerk/clerk-js': patch
+'@clerk/types': patch
+---
+
+Adjusts the layout of the `PricingTable` plan cards
diff --git a/integration/tests/pricing-table.test.ts b/integration/tests/pricing-table.test.ts
index 84e9be11fa8..365a0d44611 100644
--- a/integration/tests/pricing-table.test.ts
+++ b/integration/tests/pricing-table.test.ts
@@ -28,7 +28,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
     await expect(u.po.page.getByRole('heading', { name: 'Pro' })).toBeVisible();
   });
 
-  test('when signed out, clicking get started button navigates to sign in page', async ({ page, context }) => {
+  test('when signed out, clicking subscribe button navigates to sign in page', async ({ page, context }) => {
     const u = createTestUtils({ app, page, context });
     await u.po.page.goToRelative('/pricing-table');
 
@@ -101,7 +101,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
     await u.po.pricingTable.clickManageSubscription();
     await u.po.page.getByRole('button', { name: 'Cancel subscription' }).click();
     await u.po.page.getByRole('alertdialog').getByRole('button', { name: 'Cancel subscription' }).click();
-    await expect(u.po.page.getByRole('button', { name: 'Re-subscribe' }).first()).toBeVisible();
+    await expect(u.po.page.getByRole('button', { name: /resubscribe|re-subscribe/i }).first()).toBeVisible();
   });
 
   test.describe('in UserProfile', () => {
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index 09f6fd140cf..0c6154cc81d 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -4,7 +4,7 @@
     { "path": "./dist/clerk.browser.js", "maxSize": "68KB" },
     { "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" },
     { "path": "./dist/clerk.headless*.js", "maxSize": "52KB" },
-    { "path": "./dist/ui-common*.js", "maxSize": "102.5KB" },
+    { "path": "./dist/ui-common*.js", "maxSize": "104KB" },
     { "path": "./dist/vendors*.js", "maxSize": "39KB" },
     { "path": "./dist/coinbase*.js", "maxSize": "38KB" },
     { "path": "./dist/createorganization*.js", "maxSize": "5KB" },
diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
index 7cf0dec27b5..99140af518f 100644
--- a/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
+++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
@@ -1,4 +1,4 @@
-import { useClerk } from '@clerk/shared/react';
+import { useClerk, useUser } from '@clerk/shared/react';
 import type {
   __experimental_CommercePlanResource,
   __experimental_CommerceSubscriptionPlanPeriod,
@@ -8,7 +8,6 @@ import * as React from 'react';
 
 import { usePlansContext, usePricingTableContext, useSubscriberTypeContext } from '../../contexts';
 import {
-  Badge,
   Box,
   Button,
   Col,
@@ -20,12 +19,9 @@ import {
   SimpleButton,
   Span,
   Text,
-  useAppearance,
 } from '../../customizables';
-import { ReversibleContainer, SegmentedControl } from '../../elements';
-import { usePrefersReducedMotion } from '../../hooks';
-import { Check, InformationCircle, Plus } from '../../icons';
-import type { ThemableCssProp } from '../../styledSystem';
+import { Switch } from '../../elements';
+import { Check, Plus } from '../../icons';
 import { common, InternalThemeProvider } from '../../styledSystem';
 import { colors, getClosestProfileScrollBox } from '../../utils';
 interface PricingTableDefaultProps {
@@ -63,6 +59,7 @@ export function PricingTableDefault({
           '--column-width': 'max(var(--max-column-width), min(var(--grid-min-size, 10rem), 100%))',
           display: 'grid',
           gridTemplateColumns: 'repeat(auto-fit, minmax(var(--column-width), 1fr))',
+          gridTemplateRows: 'auto 1fr',
           gap: `var(--grid-gap-y, var(--grid-gap, ${t.space.$4})) var(--grid-gap, ${t.space.$4})`,
           alignItems: 'stretch',
           width: '100%',
@@ -102,16 +99,21 @@ interface CardProps {
 function Card(props: CardProps) {
   const { plan, planPeriod, setPlanPeriod, onSelect, props: pricingTableProps, isCompact = false } = props;
   const clerk = useClerk();
-  const { mode = 'mounted' } = usePricingTableContext();
+  const { isSignedIn } = useUser();
+  const { mode = 'mounted', ctaPosition: ctxCtaPosition } = usePricingTableContext();
   const subscriberType = useSubscriberTypeContext();
 
-  const ctaPosition = pricingTableProps.ctaPosition || 'bottom';
+  const ctaPosition = pricingTableProps.ctaPosition || ctxCtaPosition || 'bottom';
   const collapseFeatures = pricingTableProps.collapseFeatures || false;
-  const { id, slug, features } = plan;
-  const totalFeatures = features.length;
-  const hasFeatures = totalFeatures > 0;
+  const { id, slug } = plan;
 
-  const { buttonPropsForPlan, isDefaultPlanImplicitlyActiveOrUpcoming } = usePlansContext();
+  const { buttonPropsForPlan, isDefaultPlanImplicitlyActiveOrUpcoming, activeOrUpcomingSubscription, subscriptions } =
+    usePlansContext();
+
+  const subscription = activeOrUpcomingSubscription(plan);
+  const isImplicitlyActiveOrUpcoming = isDefaultPlanImplicitlyActiveOrUpcoming && plan.isDefault;
+
+  const showStatusRow = !!subscription || (isImplicitlyActiveOrUpcoming && isSignedIn);
 
   const showPlanDetails = (event?: React.MouseEvent<HTMLElement>) => {
     const portalRoot = getClosestProfileScrollBox(mode, event);
@@ -130,8 +132,10 @@ function Card(props: CardProps) {
       elementDescriptor={descriptors.pricingTableCard}
       elementId={descriptors.pricingTableCard.setId(slug)}
       sx={t => ({
-        display: 'flex',
-        flexDirection: 'column',
+        display: 'grid',
+        gap: 0,
+        gridTemplateRows: 'subgrid',
+        gridRow: 'span 4',
         background: common.mergedColorsBackground(
           colors.setAlpha(t.colors.$colorBackground, 1),
           t.colors.$neutralAlpha50,
@@ -152,7 +156,14 @@ function Card(props: CardProps) {
         planPeriod={planPeriod}
         setPlanPeriod={setPlanPeriod}
       />
-      <ReversibleContainer reverse={ctaPosition === 'top'}>
+      <Box
+        elementDescriptor={descriptors.pricingTableCardBody}
+        sx={{
+          display: 'flex',
+          flexDirection: 'column',
+          gap: 0,
+        }}
+      >
         {!collapseFeatures ? (
           <Box
             elementDescriptor={descriptors.pricingTableCardFeatures}
@@ -160,9 +171,9 @@ function Card(props: CardProps) {
               display: 'flex',
               flexDirection: 'column',
               flex: '1',
-              padding: hasFeatures ? (isCompact ? t.space.$3 : t.space.$4) : 0,
-              backgroundColor: hasFeatures ? t.colors.$colorBackground : undefined,
-              borderTopWidth: hasFeatures ? t.borderWidths.$normal : 0,
+              padding: isCompact ? t.space.$3 : t.space.$4,
+              backgroundColor: t.colors.$colorBackground,
+              borderTopWidth: t.borderWidths.$normal,
               borderTopStyle: t.borderStyles.$solid,
               borderTopColor: t.colors.$neutralAlpha100,
             })}
@@ -175,19 +186,58 @@ function Card(props: CardProps) {
             />
           </Box>
         ) : null}
+
+        {showStatusRow && (
+          <Box
+            elementDescriptor={descriptors.pricingTableCardStatusRow}
+            sx={t => ({
+              padding: t.space.$1,
+              borderTopWidth: t.borderWidths.$normal,
+              borderTopStyle: t.borderStyles.$solid,
+              borderTopColor: t.colors.$neutralAlpha100,
+            })}
+          >
+            {subscription?.status === 'active' || (isImplicitlyActiveOrUpcoming && subscriptions.length === 0) ? (
+              <Text
+                elementDescriptor={descriptors.pricingTableCardStatus}
+                variant='caption'
+                colorScheme='body'
+                localizationKey={localizationKeys('badge__currentPlan')}
+                sx={{ textAlign: 'center' }}
+              />
+            ) : (
+              <Text
+                elementDescriptor={descriptors.pricingTableCardStatus}
+                variant='caption'
+                colorScheme='body'
+                localizationKey={
+                  subscription
+                    ? localizationKeys('badge__startsAt', {
+                        date: subscription.periodStart,
+                      })
+                    : localizationKeys('badge__upcomingPlan')
+                }
+                sx={{ textAlign: 'center' }}
+              />
+            )}
+          </Box>
+        )}
+
         {(!plan.isDefault || !isDefaultPlanImplicitlyActiveOrUpcoming) && (
           <Box
             elementDescriptor={descriptors.pricingTableCardAction}
             sx={t => ({
               marginTop: 'auto',
               padding: isCompact ? t.space.$3 : t.space.$4,
-              borderTopWidth: hasFeatures ? t.borderWidths.$normal : 0,
+              borderTopWidth: t.borderWidths.$normal,
               borderTopStyle: t.borderStyles.$solid,
               borderTopColor: t.colors.$neutralAlpha100,
               background: undefined,
+              order: ctaPosition === 'top' ? -1 : undefined,
             })}
           >
             <Button
+              elementDescriptor={descriptors.pricingTableCardActionButton}
               block
               textVariant={isCompact ? 'buttonSmall' : 'buttonLarge'}
               {...buttonPropsForPlan({ plan, isCompact })}
@@ -197,7 +247,7 @@ function Card(props: CardProps) {
             />
           </Box>
         )}
-      </ReversibleContainer>
+      </Box>
     </Box>
   );
 }
@@ -214,16 +264,9 @@ interface CardHeaderProps {
 }
 
 const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>((props, ref) => {
-  const prefersReducedMotion = usePrefersReducedMotion();
-  const { animations: layoutAnimations } = useAppearance().parsedLayout;
   const { plan, isCompact, planPeriod, setPlanPeriod } = props;
   const { name, annualMonthlyAmount } = plan;
-  const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
-  const pricingTableCardFeePeriodNoticeAnimation: ThemableCssProp = t => ({
-    transition: isMotionSafe
-      ? `grid-template-rows ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`
-      : 'none',
-  });
+
   const getPlanFee = React.useMemo(() => {
     if (annualMonthlyAmount <= 0) {
       return plan.amountFormatted;
@@ -231,12 +274,6 @@ const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>((props, ref
     return planPeriod === 'annual' ? plan.annualMonthlyAmountFormatted : plan.amountFormatted;
   }, [annualMonthlyAmount, planPeriod, plan.amountFormatted, plan.annualMonthlyAmountFormatted]);
 
-  const { activeOrUpcomingSubscription, isDefaultPlanImplicitlyActiveOrUpcoming, subscriptions } = usePlansContext();
-  const subscription = activeOrUpcomingSubscription(plan);
-  const isImplicitlyActiveOrUpcoming = isDefaultPlanImplicitlyActiveOrUpcoming && plan.isDefault;
-
-  const showBadge = !!subscription || isImplicitlyActiveOrUpcoming;
-
   return (
     <Box
       ref={ref}
@@ -244,76 +281,40 @@ const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>((props, ref
       sx={t => ({
         width: '100%',
         padding: isCompact ? t.space.$3 : t.space.$4,
+        display: 'grid',
+        gap: t.space.$1,
+        gridRow: 'span 3',
+        gridTemplateRows: 'subgrid',
       })}
       data-variant={isCompact ? 'compact' : 'default'}
     >
-      <Box
-        elementDescriptor={descriptors.pricingTableCardBadgeTitleContainer}
-        sx={t => ({
-          width: '100%',
-          display: 'flex',
-          flexDirection: 'row-reverse',
-          alignItems: 'baseline',
-          justifyContent: 'flex-end',
-          flexWrap: 'wrap',
-          gap: t.space.$3,
-          marginBlockEnd: t.space.$3,
-        })}
-      >
-        {showBadge ? (
-          <Span
-            elementDescriptor={descriptors.pricingTableCardBadgeContainer}
-            sx={{
-              flex: '0 0 auto',
-            }}
-          >
-            {subscription?.status === 'active' || (isImplicitlyActiveOrUpcoming && subscriptions.length === 0) ? (
-              <Badge
-                elementDescriptor={descriptors.pricingTableCardBadge}
-                localizationKey={localizationKeys('badge__currentPlan')}
-                colorScheme={'secondary'}
-              />
-            ) : (
-              <Badge
-                elementDescriptor={descriptors.pricingTableCardBadge}
-                localizationKey={
-                  subscription
-                    ? localizationKeys('badge__startsAt', {
-                        date: subscription.periodStart,
-                      })
-                    : localizationKeys('badge__upcomingPlan')
-                }
-                colorScheme={'primary'}
-              />
-            )}
-          </Span>
-        ) : null}
+      <Box elementDescriptor={descriptors.pricingTableCardTitleContainer}>
         <Heading
           elementDescriptor={descriptors.pricingTableCardTitle}
           as='h2'
           textVariant={isCompact ? 'h3' : 'h2'}
-          sx={{ flex: '1 1 auto' }}
         >
           {name}
         </Heading>
+        {!isCompact && plan.description ? (
+          <Text
+            elementDescriptor={descriptors.pricingTableCardDescription}
+            variant='subtitle'
+            colorScheme='secondary'
+          >
+            {plan.description}
+          </Text>
+        ) : null}
       </Box>
-      {!isCompact && plan.description ? (
-        <Text
-          elementDescriptor={descriptors.pricingTableCardDescription}
-          variant='subtitle'
-          colorScheme='secondary'
-        >
-          {plan.description}
-        </Text>
-      ) : null}
+
       <Flex
         elementDescriptor={descriptors.pricingTableCardFeeContainer}
         data-variant={isCompact ? 'compact' : 'default'}
         align='center'
         wrap='wrap'
         sx={t => ({
-          marginTop: isCompact ? t.space.$2 : t.space.$3,
-          columnGap: t.space.$1x5,
+          columnGap: t.space.$1,
+          marginTop: t.space.$1,
         })}
       >
         <Text
@@ -324,89 +325,51 @@ const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>((props, ref
           {plan.currencySymbol}
           {getPlanFee}
         </Text>
-        <Text
-          elementDescriptor={descriptors.pricingTableCardFeePeriod}
-          variant='caption'
-          colorScheme='secondary'
-          sx={t => ({
-            textTransform: 'lowercase',
-            ':before': {
-              content: '"/"',
-              marginInlineEnd: t.space.$1,
-            },
-          })}
-          localizationKey={localizationKeys('__experimental_commerce.month')}
-        />
-        {annualMonthlyAmount > 0 ? (
-          <Box
-            elementDescriptor={descriptors.pricingTableCardFeePeriodNotice}
-            sx={[
-              _ => ({
-                width: '100%',
-                display: 'grid',
-                gridTemplateRows: planPeriod === 'annual' ? '1fr' : '0fr',
-              }),
-              pricingTableCardFeePeriodNoticeAnimation,
-            ]}
-            // @ts-ignore - Needed until React 19 support
-            inert={planPeriod !== 'annual' ? 'true' : undefined}
-          >
-            <Box
-              elementDescriptor={descriptors.pricingTableCardFeePeriodNoticeInner}
-              sx={{
-                overflow: 'hidden',
-                minHeight: 0,
-              }}
-            >
-              <Text
-                elementDescriptor={descriptors.pricingTableCardFeePeriodNoticeLabel}
-                variant='caption'
-                colorScheme='secondary'
-                sx={t => ({
-                  width: '100%',
-                  display: 'flex',
-                  alignItems: 'center',
-                  columnGap: t.space.$1,
-                })}
-              >
-                <Icon
-                  icon={InformationCircle}
-                  colorScheme='neutral'
-                  size='sm'
-                  aria-hidden
-                />{' '}
-                <Span localizationKey={localizationKeys('__experimental_commerce.billedAnnually')} />
-              </Text>
-            </Box>
-          </Box>
+        {!plan.isDefault ? (
+          <Text
+            elementDescriptor={descriptors.pricingTableCardFeePeriod}
+            variant='caption'
+            colorScheme='secondary'
+            sx={t => ({
+              textTransform: 'lowercase',
+              ':before': {
+                content: '"/"',
+                marginInlineEnd: t.space.$0x25,
+              },
+            })}
+            localizationKey={localizationKeys('__experimental_commerce.month')}
+          />
         ) : null}
       </Flex>
-      {plan.hasBaseFee && annualMonthlyAmount > 0 && setPlanPeriod ? (
+
+      {annualMonthlyAmount > 0 && setPlanPeriod ? (
         <Box
           elementDescriptor={descriptors.pricingTableCardPeriodToggle}
           sx={t => ({
-            display: 'flex',
-            marginTop: t.space.$3,
+            marginTop: t.space.$1,
           })}
         >
-          <SegmentedControl.Root
-            aria-label='Set pay period'
-            value={planPeriod}
-            onChange={value => setPlanPeriod(value as __experimental_CommerceSubscriptionPlanPeriod)}
-          >
-            <SegmentedControl.Button
-              value='month'
-              // TODO(@Commerce): needs localization
-              text='Monthly'
-            />
-            <SegmentedControl.Button
-              value='annual'
-              // TODO(@Commerce): needs localization
-              text='Annually'
-            />
-          </SegmentedControl.Root>
+          <Switch
+            checked={planPeriod === 'annual'}
+            onChange={(checked: boolean) => setPlanPeriod(checked ? 'annual' : 'month')}
+            label={localizationKeys('__experimental_commerce.billedAnnually')}
+          />
         </Box>
-      ) : null}
+      ) : (
+        <Text
+          elementDescriptor={descriptors.pricingTableCardFeePeriodNotice}
+          variant='caption'
+          colorScheme='secondary'
+          localizationKey={
+            plan.isDefault
+              ? localizationKeys('__experimental_commerce.alwaysFree')
+              : localizationKeys('__experimental_commerce.billedMonthlyOnly')
+          }
+          sx={{
+            alignSelf: 'center',
+          }}
+        />
+      )}
     </Box>
   );
 });
@@ -436,7 +399,7 @@ const CardFeaturesList = React.forwardRef<HTMLDivElement, CardFeaturesListProps>
       elementDescriptor={descriptors.pricingTableCardFeatures}
       sx={t => ({
         display: 'grid',
-        flex: '1',
+        flex: 1,
         rowGap: isCompact ? t.space.$2 : t.space.$3,
       })}
     >
diff --git a/packages/clerk-js/src/ui/contexts/components/Plans.tsx b/packages/clerk-js/src/ui/contexts/components/Plans.tsx
index d61d22eccf1..ec94597c8a7 100644
--- a/packages/clerk-js/src/ui/contexts/components/Plans.tsx
+++ b/packages/clerk-js/src/ui/contexts/components/Plans.tsx
@@ -32,7 +32,7 @@ export const useSubscriptions = (subscriberType?: __experimental_CommerceSubscri
 export const PlansContextProvider = ({ children }: PropsWithChildren) => {
   const { __experimental_commerce } = useClerk();
   const { organization } = useOrganization();
-  const { user } = useUser();
+  const { user, isSignedIn } = useUser();
   const subscriberType = useSubscriberTypeContext();
   const {
     data: subscriptions,
@@ -68,11 +68,18 @@ export const PlansContextProvider = ({ children }: PropsWithChildren) => {
     revalidateInvoices();
   }, [revalidateInvoices, revalidatePlans, revalidateSubscriptions]);
 
+  const isLoaded = useMemo(() => {
+    if (isSignedIn) {
+      return isLoadingSubscriptions === false && isLoadingPlans === false;
+    }
+    return isLoadingPlans === false;
+  }, [isLoadingPlans, isLoadingSubscriptions, isSignedIn]);
+
   return (
     <PlansContext.Provider
       value={{
         componentName: 'Plans',
-        plans: plans || [],
+        plans: isLoaded ? (plans ?? []) : [],
         subscriptions: subscriptions?.data || [],
         isLoading: isLoadingSubscriptions || isLoadingPlans || false,
         revalidate,
@@ -152,7 +159,7 @@ export const usePlansContext = () => {
           : // If there are no active or grace period subscriptions, show the get started button
             ctx.subscriptions.length > 0
             ? localizationKeys('__experimental_commerce.switchPlan')
-            : localizationKeys('__experimental_commerce.getStarted'),
+            : localizationKeys('__experimental_commerce.subscribe'),
         variant: isCompact || !!subscription ? 'bordered' : 'solid',
         colorScheme: isCompact || !!subscription ? 'secondary' : 'primary',
       };
diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
index e7b2601a08d..4575bb3a936 100644
--- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
+++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
@@ -243,26 +243,24 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
   'pricingTable',
   'pricingTableCard',
   'pricingTableCardHeader',
+  'pricingTableCardTitleContainer',
   'pricingTableCardTitle',
   'pricingTableCardDescription',
-  'pricingTableCardBadgeTitleContainer',
-  'pricingTableCardAvatar',
-  'pricingTableCardBadgeContainer',
-  'pricingTableCardBadge',
+  'pricingTableCardBody',
+  'pricingTableCardStatusRow',
+  'pricingTableCardStatus',
   'pricingTableCardFeatures',
   'pricingTableCardFeaturesList',
   'pricingTableCardFeaturesListItem',
   'pricingTableCardFeaturesListItemContent',
   'pricingTableCardFeaturesListItemTitle',
-  'pricingTableCardFeaturesListItemDescription',
   'pricingTableCardAction',
+  'pricingTableCardActionButton',
   'pricingTableCardPeriodToggle',
   'pricingTableCardFeeContainer',
   'pricingTableCardFee',
   'pricingTableCardFeePeriod',
   'pricingTableCardFeePeriodNotice',
-  'pricingTableCardFeePeriodNoticeInner',
-  'pricingTableCardFeePeriodNoticeLabel',
 
   'pricingTableMatrix',
   'pricingTableMatrixTable',
@@ -311,6 +309,11 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
   'segmentedControlRoot',
   'segmentedControlButton',
 
+  'switchRoot',
+  'switchIndicator',
+  'switchThumb',
+  'switchLabel',
+
   'alert',
   'alertIcon',
   'alertText',
diff --git a/packages/clerk-js/src/ui/elements/Switch.tsx b/packages/clerk-js/src/ui/elements/Switch.tsx
new file mode 100644
index 00000000000..a9fb9ffb50f
--- /dev/null
+++ b/packages/clerk-js/src/ui/elements/Switch.tsx
@@ -0,0 +1,110 @@
+import React, { forwardRef, useState } from 'react';
+
+import type { LocalizationKey } from '../customizables';
+import { descriptors, Flex, Text } from '../customizables';
+import { common } from '../styledSystem';
+
+interface SwitchProps {
+  checked?: boolean;
+  defaultChecked?: boolean;
+  onChange?: (checked: boolean) => void;
+  disabled?: boolean;
+  'aria-label'?: string;
+  label?: string | LocalizationKey;
+}
+
+export const Switch = forwardRef<HTMLDivElement, SwitchProps>(
+  ({ checked: controlledChecked, defaultChecked, onChange, disabled = false, label }, ref) => {
+    const [internalChecked, setInternalChecked] = useState(!!defaultChecked);
+    const isControlled = controlledChecked !== undefined;
+    const checked = isControlled ? controlledChecked : internalChecked;
+
+    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (disabled) return;
+      if (!isControlled) {
+        setInternalChecked(e.target.checked);
+      }
+      onChange?.(e.target.checked);
+    };
+
+    return (
+      <Flex
+        ref={ref}
+        elementDescriptor={descriptors.switchRoot}
+        direction='row'
+        align='center'
+        as='label'
+        sx={t => ({
+          width: 'fit-content',
+          '&:has(input:focus-visible) > input + span': {
+            ...common.focusRingStyles(t),
+          },
+        })}
+      >
+        {/* The order of the elements is important here for the focus ring to work. The input is visually hidden, so the focus ring is applied to the span. */}
+        <input
+          type='checkbox'
+          role='switch'
+          disabled={disabled}
+          defaultChecked={defaultChecked}
+          checked={checked}
+          onChange={handleChange}
+          style={{
+            ...common.visuallyHidden(),
+          }}
+        />
+        <Flex
+          elementDescriptor={descriptors.switchIndicator}
+          as='span'
+          data-checked={checked}
+          sx={t => ({
+            width: t.sizes.$6,
+            height: t.sizes.$4,
+            alignItems: 'center',
+            position: 'relative',
+            backgroundColor: checked ? t.colors.$primary500 : t.colors.$neutralAlpha150,
+            borderRadius: 999,
+            transition: 'background-color 0.2s',
+            opacity: disabled ? 0.6 : 1,
+            cursor: disabled ? 'not-allowed' : 'pointer',
+            outline: 'none',
+            boxSizing: 'border-box',
+            boxShadow: '0px 0px 0px 1px rgba(0, 0, 0, 0.06) inset',
+          })}
+        >
+          <Flex
+            as='span'
+            elementDescriptor={descriptors.switchThumb}
+            sx={t => ({
+              position: 'absolute',
+              left: t.sizes.$0x5,
+              width: t.sizes.$3,
+              height: t.sizes.$3,
+              borderRadius: '50%',
+              backgroundColor: t.colors.$colorBackground,
+              boxShadow: t.shadows.$switchControl,
+              transform: `translateX(${checked ? t.sizes.$2 : 0})`,
+              transition: 'transform 0.2s',
+              zIndex: 1,
+            })}
+          />
+        </Flex>
+        {label && (
+          <Text
+            as='span'
+            variant='caption'
+            colorScheme='secondary'
+            localizationKey={label}
+            sx={t => ({
+              paddingInlineStart: t.sizes.$2,
+              cursor: disabled ? 'not-allowed' : 'pointer',
+              userSelect: 'none',
+            })}
+          />
+        )}
+      </Flex>
+    );
+  },
+);
+
+Switch.displayName = 'Switch';
diff --git a/packages/clerk-js/src/ui/elements/index.ts b/packages/clerk-js/src/ui/elements/index.ts
index 6914c722e5a..aa2272771e5 100644
--- a/packages/clerk-js/src/ui/elements/index.ts
+++ b/packages/clerk-js/src/ui/elements/index.ts
@@ -50,6 +50,7 @@ export * from './Section';
 export * from './SegmentedControl';
 export * from './Select';
 export * from './SuccessPage';
+export * from './Switch';
 export * from './Tabs';
 export * from './TagInput';
 export * from './ThreeDotsMenu';
diff --git a/packages/clerk-js/src/ui/foundations/shadows.ts b/packages/clerk-js/src/ui/foundations/shadows.ts
index 3c1327b06b0..490215ff011 100644
--- a/packages/clerk-js/src/ui/foundations/shadows.ts
+++ b/packages/clerk-js/src/ui/foundations/shadows.ts
@@ -12,4 +12,6 @@ export const shadows = Object.freeze({
   badge: '0px 2px 0px -1px rgba(0, 0, 0, 0.04)',
   tableBodyShadow: '0px 0px 1px 0px rgba(0, 0, 0, 0.08), 0px 1px 2px 0px rgba(0, 0, 0, 0.12)',
   segmentedControl: '0px 1px 2px 0px rgba(0, 0, 0, 0.08)',
+  switchControl:
+    '0px 2px 2px -1px rgba(0, 0, 0, 0.06), 0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 4px 4px -2px rgba(0, 0, 0, 0.06)',
 } as const);
diff --git a/packages/clerk-js/src/ui/styledSystem/common.ts b/packages/clerk-js/src/ui/styledSystem/common.ts
index b60230e10c2..471711397ab 100644
--- a/packages/clerk-js/src/ui/styledSystem/common.ts
+++ b/packages/clerk-js/src/ui/styledSystem/common.ts
@@ -130,15 +130,21 @@ const borderColor = (t: InternalTheme, props?: any) => {
   } as const;
 };
 
+const focusRingStyles = (t: InternalTheme) => {
+  return {
+    '&::-moz-focus-inner': { border: '0' },
+    WebkitTapHighlightColor: 'transparent',
+    boxShadow: t.shadows.$focusRing.replace('{{color}}', t.colors.$neutralAlpha200),
+    transitionProperty: t.transitionProperty.$common,
+    transitionTimingFunction: t.transitionTiming.$common,
+    transitionDuration: t.transitionDuration.$focusRing,
+  } as const;
+};
+
 const focusRing = (t: InternalTheme) => {
   return {
     '&:focus': {
-      '&::-moz-focus-inner': { border: '0' },
-      WebkitTapHighlightColor: 'transparent',
-      boxShadow: t.shadows.$focusRing.replace('{{color}}', t.colors.$neutralAlpha200),
-      transitionProperty: t.transitionProperty.$common,
-      transitionTimingFunction: t.transitionTiming.$common,
-      transitionDuration: t.transitionDuration.$focusRing,
+      ...focusRingStyles(t),
     },
   } as const;
 };
@@ -198,6 +204,7 @@ const visuallyHidden = () =>
 export const common = {
   textVariants,
   borderVariants,
+  focusRingStyles,
   focusRing,
   disabled,
   borderColor,
diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts
index be67bbac93e..23e28281bc8 100644
--- a/packages/localizations/src/en-US.ts
+++ b/packages/localizations/src/en-US.ts
@@ -5,6 +5,8 @@ export const enUS: LocalizationResource = {
   __experimental_commerce: {
     availableFeatures: 'Available features',
     billedAnnually: 'Billed annually',
+    billedMonthlyOnly: 'Only billed monthly',
+    alwaysFree: 'Always free',
     cancelSubscription: 'Cancel subscription',
     checkout: {
       description__paymentSuccessful: 'Your new subscription is all set.',
@@ -29,15 +31,16 @@ export const enUS: LocalizationResource = {
     manage: 'Manage',
     manageSubscription: 'Manage subscription',
     month: 'Month',
-    reSubscribe: 'Re-subscribe',
+    reSubscribe: 'Resubscribe',
     seeAllFeatures: 'See all features',
+    subscribe: 'Subscribe',
     switchPlan: 'Switch to this plan',
     switchToAnnual: 'Switch to annual',
     viewFeatures: 'View features',
   },
   backButton: 'Back',
   badge__canceledEndsAt: "Canceled • Ends {{ date | shortDate('en-US') }}",
-  badge__currentPlan: 'Current Plan',
+  badge__currentPlan: 'Current plan',
   badge__default: 'Default',
   badge__endsAt: "Ends {{ date | shortDate('en-US') }}",
   badge__expired: 'Expired',
@@ -714,7 +717,7 @@ export const enUS: LocalizationResource = {
       subscriptionsSection: {
         actionLabel__default: 'Manage',
       },
-      title: 'Billing & Payments',
+      title: 'Billing',
     },
     backupCodePage: {
       actionLabel__copied: 'Copied!',
diff --git a/packages/testing/src/playwright/unstable/page-objects/pricingTable.ts b/packages/testing/src/playwright/unstable/page-objects/pricingTable.ts
index e3e426ac417..7a2fc9bd560 100644
--- a/packages/testing/src/playwright/unstable/page-objects/pricingTable.ts
+++ b/packages/testing/src/playwright/unstable/page-objects/pricingTable.ts
@@ -16,8 +16,14 @@ export const createPricingTablePageObject = (testArgs: { page: EnhancedPage }) =
     },
     startCheckout: async ({ planSlug, shouldSwitch }: { planSlug: string; shouldSwitch?: boolean }) => {
       const targetButtonName =
-        shouldSwitch === true ? 'Switch to this plan' : shouldSwitch === false ? 'Get started' : /get|switch/i;
-      await page.locator(`.cl-pricingTableCard__${planSlug}`).getByRole('button', { name: targetButtonName }).click();
+        shouldSwitch === true ? 'Switch to this plan' : shouldSwitch === false ? /subscribe/i : /get|switch|subscribe/i;
+
+      await page
+        .locator(`.cl-pricingTableCard__${planSlug} .cl-pricingTableCardAction`)
+        .getByRole('button', {
+          name: targetButtonName,
+        })
+        .click();
     },
   };
   return self;
diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts
index 56209fc7eaf..292fa51a751 100644
--- a/packages/types/src/appearance.ts
+++ b/packages/types/src/appearance.ts
@@ -282,6 +282,11 @@ export type ElementsConfig = {
   segmentedControlRoot: WithOptions;
   segmentedControlButton: WithOptions;
 
+  switchRoot: WithOptions;
+  switchIndicator: WithOptions;
+  switchThumb: WithOptions;
+  switchLabel: WithOptions;
+
   avatarBox: WithOptions;
   avatarImage: WithOptions;
   avatarImageActions: WithOptions;
@@ -370,26 +375,24 @@ export type ElementsConfig = {
   pricingTable: WithOptions;
   pricingTableCard: WithOptions<string>;
   pricingTableCardHeader: WithOptions;
-  pricingTableCardBadgeTitleContainer: WithOptions;
-  pricingTableCardAvatar: WithOptions;
-  pricingTableCardBadgeContainer: WithOptions;
-  pricingTableCardBadge: WithOptions;
+  pricingTableCardTitleContainer: WithOptions;
   pricingTableCardTitle: WithOptions;
   pricingTableCardDescription: WithOptions;
+  pricingTableCardFeeContainer: WithOptions;
+  pricingTableCardFee: WithOptions;
+  pricingTableCardFeePeriod: WithOptions;
+  pricingTableCardPeriodToggle: WithOptions;
+  pricingTableCardFeePeriodNotice: WithOptions;
+  pricingTableCardBody: WithOptions;
   pricingTableCardFeatures: WithOptions;
   pricingTableCardFeaturesList: WithOptions<string>;
   pricingTableCardFeaturesListItem: WithOptions<string>;
   pricingTableCardFeaturesListItemContent: WithOptions;
   pricingTableCardFeaturesListItemTitle: WithOptions;
-  pricingTableCardFeaturesListItemDescription: WithOptions;
+  pricingTableCardStatusRow: WithOptions;
+  pricingTableCardStatus: WithOptions;
   pricingTableCardAction: WithOptions;
-  pricingTableCardPeriodToggle: WithOptions;
-  pricingTableCardFeeContainer: WithOptions;
-  pricingTableCardFee: WithOptions;
-  pricingTableCardFeePeriod: WithOptions;
-  pricingTableCardFeePeriodNotice: WithOptions;
-  pricingTableCardFeePeriodNoticeInner: WithOptions;
-  pricingTableCardFeePeriodNoticeLabel: WithOptions;
+  pricingTableCardActionButton: WithOptions;
 
   pricingTableMatrix: WithOptions;
   pricingTableMatrixTable: WithOptions;
diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts
index b398e7c30cd..969ab226865 100644
--- a/packages/types/src/localization.ts
+++ b/packages/types/src/localization.ts
@@ -113,9 +113,12 @@ type _LocalizationResource = {
     cancelSubscription: LocalizationValue;
     keepSubscription: LocalizationValue;
     reSubscribe: LocalizationValue;
+    subscribe: LocalizationValue;
     switchPlan: LocalizationValue;
     switchToAnnual: LocalizationValue;
     billedAnnually: LocalizationValue;
+    billedMonthlyOnly: LocalizationValue;
+    alwaysFree: LocalizationValue;
     accountFunds: LocalizationValue;
     defaultFreePlanActive: LocalizationValue;
     viewFeatures: LocalizationValue;


Reviews:
user: login: alexcarpenter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-04T23:18:04Z
user: login: alexcarpenter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-04T23:51:41Z
user: login: LekoArts
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T01:38:32Z
user: login: aeliox
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T02:50:51Z
user: login: aeliox
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T02:51:35Z
user: login: aeliox
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T02:53:18Z
user: login: LekoArts
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T03:08:21Z
user: login: LekoArts
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T04:08:35Z
user: login: octoper
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T08:37:50Z
user: login: octoper
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T09:41:09Z
user: login: octoper
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T13:11:09Z
user: login: octoper
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T13:11:47Z
user: login: alexcarpenter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T13:23:11Z
user: login: alexcarpenter
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-05T15:38:34Z
user: login: octoper
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-05T15:48:03Z

Review comments:
diff_hunk: @@ -0,0 +1,118 @@
+import React, { forwardRef, useState } from 'react';
+
+import type { LocalizationKey } from '../customizables';
+import { descriptors, Flex, Text } from '../customizables';
+
+interface SwitchProps {
+  checked?: boolean;
+  defaultChecked?: boolean;
+  onChange?: (checked: boolean) => void;
+  disabled?: boolean;
+  'aria-label'?: string;
+  'aria-labelledby'?: string;
+  title?: string | LocalizationKey;
+}
+
+export const Switch = forwardRef<HTMLDivElement, SwitchProps>(
+  (
+    {
+      checked: controlledChecked,
+      defaultChecked,
+      onChange,
+      disabled = false,
+      'aria-label': ariaLabel,
+      'aria-labelledby': ariaLabelledby,
+      title,
+    },
+    ref,
+  ) => {
+    const [internalChecked, setInternalChecked] = useState(!!defaultChecked);
+    const isControlled = controlledChecked !== undefined;
+    const checked = isControlled ? controlledChecked : internalChecked;
+
+    const handleToggle = (_: React.MouseEvent | React.KeyboardEvent) => {
+      if (disabled) return;
+      if (!isControlled) {
+        setInternalChecked(!checked);
+      }
+      onChange?.(!checked);
+    };
+
+    const handleKeyDown = (e: React.KeyboardEvent) => {
+      if (e.key === ' ' || e.key === 'Enter') {
+        e.preventDefault();
+        handleToggle(e);
+      }
+    };
+
+    return (
+      <Flex
+        direction='row'
+        align='center'
+        as='div'
+        ref={ref}
+        tabIndex={disabled ? -1 : 0}
+        role='switch'
+        aria-checked={checked}
+        aria-label={ariaLabel}
+        aria-labelledby={ariaLabelledby}
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: alexcarpenter
body: I think we don't need one or the other here. `ariaLabelledby` would be applied to the text element right?
created_at: 2025-05-04T23:02:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+import React, { forwardRef, useState } from 'react';
+
+import type { LocalizationKey } from '../customizables';
+import { descriptors, Flex, Text } from '../customizables';
+
+interface SwitchProps {
+  checked?: boolean;
+  defaultChecked?: boolean;
+  onChange?: (checked: boolean) => void;
+  disabled?: boolean;
+  'aria-label'?: string;
+  'aria-labelledby'?: string;
+  title?: string | LocalizationKey;
+}
+
+export const Switch = forwardRef<HTMLDivElement, SwitchProps>(
+  (
+    {
+      checked: controlledChecked,
+      defaultChecked,
+      onChange,
+      disabled = false,
+      'aria-label': ariaLabel,
+      'aria-labelledby': ariaLabelledby,
+      title,
+    },
+    ref,
+  ) => {
+    const [internalChecked, setInternalChecked] = useState(!!defaultChecked);
+    const isControlled = controlledChecked !== undefined;
+    const checked = isControlled ? controlledChecked : internalChecked;
+
+    const handleToggle = (_: React.MouseEvent | React.KeyboardEvent) => {
+      if (disabled) return;
+      if (!isControlled) {
+        setInternalChecked(!checked);
+      }
+      onChange?.(!checked);
+    };
+
+    const handleKeyDown = (e: React.KeyboardEvent) => {
+      if (e.key === ' ' || e.key === 'Enter') {
+        e.preventDefault();
+        handleToggle(e);
+      }
+    };
+
+    return (
+      <Flex
+        direction='row'
+        align='center'
+        as='div'
+        ref={ref}
+        tabIndex={disabled ? -1 : 0}
+        role='switch'
+        aria-checked={checked}
+        aria-label={ariaLabel}
+        aria-labelledby={ariaLabelledby}
+        elementDescriptor={descriptors.switchRoot}
+        onClick={handleToggle}
+        onKeyDown={handleKeyDown}
+        sx={{
+          width: 'fit-content',
+          userSelect: 'none',
+        }}
+      >
+        <Flex
+          as='div'
+          align='center'
+          sx={t => ({
+            width: 24,
+            height: 16,
+            alignItems: 'center',
+            position: 'relative',
+            backgroundColor: checked ? t.colors.$primary500 : t.colors.$neutralAlpha150,
+            borderRadius: 999,
+            transition: 'background-color 0.2s',
+            opacity: disabled ? 0.6 : 1,
+            cursor: disabled ? 'not-allowed' : 'pointer',
+            outline: 'none',
+            boxSizing: 'border-box',
+            boxShadow: '0px 0px 0px 1px rgba(0, 0, 0, 0.06) inset',
+          })}
+        >
+          <Flex
+            as='span'
+            elementDescriptor={descriptors.switchThumb}
+            sx={t => ({
+              position: 'absolute',
+              left: checked ? 10 : 2,
+              width: 12,
+              height: 12,
+              borderRadius: '50%',
+              backgroundColor: t.colors.$colorBackground,
+              boxShadow: t.shadows.$switchControl,
+              transition: 'left 0.2s',
+              zIndex: 1,
+            })}
+          />
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: alexcarpenter
body: Can we use transforms vs left positioning? and also use our sizes tokens vs using pixels


```tsx
<Flex
  as='div'
  align='center'
  sx={t => ({
    width: t.sizes.$6,
    height: t.sizes.$4,
    alignItems: 'center',
    position: 'relative',
    backgroundColor: checked ? t.colors.$primary500 : t.colors.$neutralAlpha150,
    borderRadius: 999,
    transition: 'background-color 0.2s',
    opacity: disabled ? 0.6 : 1,
    cursor: disabled ? 'not-allowed' : 'pointer',
    outline: 'none',
    boxSizing: 'border-box',
    boxShadow: '0px 0px 0px 1px rgba(0, 0, 0, 0.06) inset',
  })}
>
  <Flex
    as='span'
    elementDescriptor={descriptors.switchThumb}
    sx={t => ({
      position: 'absolute',
      left: t.sizes.$0x5,
      width: t.sizes.$3,
      height: t.sizes.$3,
      borderRadius: '50%',
      backgroundColor: t.colors.$colorBackground,
      boxShadow: t.shadows.$switchControl,
      transform: `translateX(${checked ? t.sizes.$2 : 0})`,
      transition: 'transform 0.2s',
      zIndex: 1,
    })}
  />
</Flex>
```
created_at: 2025-05-04T23:04:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+import React, { forwardRef, useState } from 'react';
+
+import type { LocalizationKey } from '../customizables';
+import { descriptors, Flex, Text } from '../customizables';
+
+interface SwitchProps {
+  checked?: boolean;
+  defaultChecked?: boolean;
+  onChange?: (checked: boolean) => void;
+  disabled?: boolean;
+  'aria-label'?: string;
+  'aria-labelledby'?: string;
+  title?: string | LocalizationKey;
+}
+
+export const Switch = forwardRef<HTMLDivElement, SwitchProps>(
+  (
+    {
+      checked: controlledChecked,
+      defaultChecked,
+      onChange,
+      disabled = false,
+      'aria-label': ariaLabel,
+      'aria-labelledby': ariaLabelledby,
+      title,
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: alexcarpenter
body: I think label makes more sense then title in the usage.
created_at: 2025-05-04T23:04:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+import React, { forwardRef, useState } from 'react';
+
+import type { LocalizationKey } from '../customizables';
+import { descriptors, Flex, Text } from '../customizables';
+
+interface SwitchProps {
+  checked?: boolean;
+  defaultChecked?: boolean;
+  onChange?: (checked: boolean) => void;
+  disabled?: boolean;
+  'aria-label'?: string;
+  'aria-labelledby'?: string;
+  title?: string | LocalizationKey;
+}
+
+export const Switch = forwardRef<HTMLDivElement, SwitchProps>(
+  (
+    {
+      checked: controlledChecked,
+      defaultChecked,
+      onChange,
+      disabled = false,
+      'aria-label': ariaLabel,
+      'aria-labelledby': ariaLabelledby,
+      title,
+    },
+    ref,
+  ) => {
+    const [internalChecked, setInternalChecked] = useState(!!defaultChecked);
+    const isControlled = controlledChecked !== undefined;
+    const checked = isControlled ? controlledChecked : internalChecked;
+
+    const handleToggle = (_: React.MouseEvent | React.KeyboardEvent) => {
+      if (disabled) return;
+      if (!isControlled) {
+        setInternalChecked(!checked);
+      }
+      onChange?.(!checked);
+    };
+
+    const handleKeyDown = (e: React.KeyboardEvent) => {
+      if (e.key === ' ' || e.key === 'Enter') {
+        e.preventDefault();
+        handleToggle(e);
+      }
+    };
+
+    return (
+      <Flex
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: alexcarpenter
body: We need to add descriptors to the switch component
created_at: 2025-05-04T23:05:04Z
author_association: MEMBER
diff_hunk: @@ -175,16 +184,53 @@ function Card(props: CardProps) {
             />
           </Box>
         ) : null}
+
+        {showStatusRow && (
+          <Box
+            sx={t => ({
+              padding: t.space.$1,
+              borderTopWidth: t.borderWidths.$normal,
+              borderTopStyle: t.borderStyles.$solid,
+              borderTopColor: t.colors.$neutralAlpha100,
+            })}
+          >
+            {subscription?.status === 'active' || (isImplicitlyActiveOrUpcoming && subscriptions.length === 0) ? (
+              <Text
+                elementDescriptor={descriptors.pricingTableCardBadge}
+                variant='caption'
+                colorScheme='body'
+                localizationKey={localizationKeys('badge__currentPlan')}
+                sx={{ textAlign: 'center' }}
+              />
+            ) : (
+              <Text
+                elementDescriptor={descriptors.pricingTableCardBadge}
+                variant='caption'
+                colorScheme='body'
+                localizationKey={
+                  subscription
+                    ? localizationKeys('badge__startsAt', {
+                        date: subscription.periodStart,
+                      })
+                    : localizationKeys('badge__upcomingPlan')
+                }
+                sx={{ textAlign: 'center' }}
+              />
+            )}
+          </Box>
+        )}
+
         {(!plan.isDefault || !isDefaultPlanImplicitlyActiveOrUpcoming) && (
           <Box
             elementDescriptor={descriptors.pricingTableCardAction}
             sx={t => ({
               marginTop: 'auto',
               padding: isCompact ? t.space.$3 : t.space.$4,
-              borderTopWidth: hasFeatures ? t.borderWidths.$normal : 0,
+              borderTopWidth: t.borderWidths.$normal,
               borderTopStyle: t.borderStyles.$solid,
               borderTopColor: t.colors.$neutralAlpha100,
               background: undefined,
+              order: ctaPosition === 'top' ? -1 : undefined,
path: packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
user: login: alexcarpenter
body: This looks a bit off with ctaPosition as top.

![Screenshot 2025-05-04 at 7 07 18 PM](https://github.com/user-attachments/assets/d0cc3348-abf8-454f-b927-c48062ae18e9)

created_at: 2025-05-04T23:08:24Z
author_association: MEMBER
diff_hunk: @@ -175,16 +184,53 @@ function Card(props: CardProps) {
             />
           </Box>
         ) : null}
+
+        {showStatusRow && (
+          <Box
+            sx={t => ({
+              padding: t.space.$1,
+              borderTopWidth: t.borderWidths.$normal,
+              borderTopStyle: t.borderStyles.$solid,
+              borderTopColor: t.colors.$neutralAlpha100,
+            })}
+          >
+            {subscription?.status === 'active' || (isImplicitlyActiveOrUpcoming && subscriptions.length === 0) ? (
+              <Text
+                elementDescriptor={descriptors.pricingTableCardBadge}
+                variant='caption'
+                colorScheme='body'
+                localizationKey={localizationKeys('badge__currentPlan')}
+                sx={{ textAlign: 'center' }}
+              />
+            ) : (
+              <Text
+                elementDescriptor={descriptors.pricingTableCardBadge}
+                variant='caption'
+                colorScheme='body'
+                localizationKey={
+                  subscription
+                    ? localizationKeys('badge__startsAt', {
+                        date: subscription.periodStart,
+                      })
+                    : localizationKeys('badge__upcomingPlan')
+                }
+                sx={{ textAlign: 'center' }}
+              />
+            )}
+          </Box>
+        )}
+
         {(!plan.isDefault || !isDefaultPlanImplicitlyActiveOrUpcoming) && (
           <Box
             elementDescriptor={descriptors.pricingTableCardAction}
             sx={t => ({
               marginTop: 'auto',
               padding: isCompact ? t.space.$3 : t.space.$4,
-              borderTopWidth: hasFeatures ? t.borderWidths.$normal : 0,
+              borderTopWidth: t.borderWidths.$normal,
               borderTopStyle: t.borderStyles.$solid,
               borderTopColor: t.colors.$neutralAlpha100,
               background: undefined,
+              order: ctaPosition === 'top' ? -1 : undefined,
path: packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
user: login: alexcarpenter
body: Are we not able to use the `ReversibleContainer` to handle the CTA position anymore?
created_at: 2025-05-04T23:11:35Z
author_association: MEMBER
diff_hunk: @@ -8,7 +8,6 @@ import * as React from 'react';
 
path: packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
user: login: alexcarpenter
body: General note, that all of the new elements we're adding in the pricing table here should have descriptors added to them.
created_at: 2025-05-04T23:12:15Z
author_association: MEMBER
diff_hunk: @@ -175,16 +184,53 @@ function Card(props: CardProps) {
             />
           </Box>
         ) : null}
+
+        {showStatusRow && (
path: packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
user: login: alexcarpenter
body: This is still causing some considerable layout shift in this position.


https://github.com/user-attachments/assets/deb7c226-9f6a-4c47-b529-5a6cc8eceb60


created_at: 2025-05-04T23:16:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+import React, { forwardRef, useState } from 'react';
+
+import type { LocalizationKey } from '../customizables';
+import { descriptors, Flex, Text } from '../customizables';
+
+interface SwitchProps {
+  checked?: boolean;
+  defaultChecked?: boolean;
+  onChange?: (checked: boolean) => void;
+  disabled?: boolean;
+  'aria-label'?: string;
+  'aria-labelledby'?: string;
+  title?: string | LocalizationKey;
+}
+
+export const Switch = forwardRef<HTMLDivElement, SwitchProps>(
+  (
+    {
+      checked: controlledChecked,
+      defaultChecked,
+      onChange,
+      disabled = false,
+      'aria-label': ariaLabel,
+      'aria-labelledby': ariaLabelledby,
+      title,
+    },
+    ref,
+  ) => {
+    const [internalChecked, setInternalChecked] = useState(!!defaultChecked);
+    const isControlled = controlledChecked !== undefined;
+    const checked = isControlled ? controlledChecked : internalChecked;
+
+    const handleToggle = (_: React.MouseEvent | React.KeyboardEvent) => {
+      if (disabled) return;
+      if (!isControlled) {
+        setInternalChecked(!checked);
+      }
+      onChange?.(!checked);
+    };
+
+    const handleKeyDown = (e: React.KeyboardEvent) => {
+      if (e.key === ' ' || e.key === 'Enter') {
+        e.preventDefault();
+        handleToggle(e);
+      }
+    };
+
+    return (
+      <Flex
+        direction='row'
+        align='center'
+        as='div'
+        ref={ref}
+        tabIndex={disabled ? -1 : 0}
+        role='switch'
+        aria-checked={checked}
+        aria-label={ariaLabel}
+        aria-labelledby={ariaLabelledby}
+        elementDescriptor={descriptors.switchRoot}
+        onClick={handleToggle}
+        onKeyDown={handleKeyDown}
+        sx={{
+          width: 'fit-content',
+          userSelect: 'none',
+        }}
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: alexcarpenter
body: Can we update the focus-visible state to show around the switch indicator, not wrapping the whole element. And make it in line with the other interactive element focus styles?

![Screenshot 2025-05-04 at 7 16 33 PM](https://github.com/user-attachments/assets/d0179572-95ed-4068-8be6-fcbb64b16e10)

created_at: 2025-05-04T23:17:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,118 @@
+import React, { forwardRef, useState } from 'react';
+
+import type { LocalizationKey } from '../customizables';
+import { descriptors, Flex, Text } from '../customizables';
+
+interface SwitchProps {
+  checked?: boolean;
+  defaultChecked?: boolean;
+  onChange?: (checked: boolean) => void;
+  disabled?: boolean;
+  'aria-label'?: string;
+  'aria-labelledby'?: string;
+  title?: string | LocalizationKey;
+}
+
+export const Switch = forwardRef<HTMLDivElement, SwitchProps>(
+  (
+    {
+      checked: controlledChecked,
+      defaultChecked,
+      onChange,
+      disabled = false,
+      'aria-label': ariaLabel,
+      'aria-labelledby': ariaLabelledby,
+      title,
+    },
+    ref,
+  ) => {
+    const [internalChecked, setInternalChecked] = useState(!!defaultChecked);
+    const isControlled = controlledChecked !== undefined;
+    const checked = isControlled ? controlledChecked : internalChecked;
+
+    const handleToggle = (_: React.MouseEvent | React.KeyboardEvent) => {
+      if (disabled) return;
+      if (!isControlled) {
+        setInternalChecked(!checked);
+      }
+      onChange?.(!checked);
+    };
+
+    const handleKeyDown = (e: React.KeyboardEvent) => {
+      if (e.key === ' ' || e.key === 'Enter') {
+        e.preventDefault();
+        handleToggle(e);
+      }
+    };
+
+    return (
+      <Flex
+        direction='row'
+        align='center'
+        as='div'
+        ref={ref}
+        tabIndex={disabled ? -1 : 0}
+        role='switch'
+        aria-checked={checked}
+        aria-label={ariaLabel}
+        aria-labelledby={ariaLabelledby}
+        elementDescriptor={descriptors.switchRoot}
+        onClick={handleToggle}
+        onKeyDown={handleKeyDown}
+        sx={{
+          width: 'fit-content',
+          userSelect: 'none',
+        }}
+      >
+        <Flex
+          as='div'
+          align='center'
+          sx={t => ({
+            width: 24,
+            height: 16,
+            alignItems: 'center',
+            position: 'relative',
+            backgroundColor: checked ? t.colors.$primary500 : t.colors.$neutralAlpha150,
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: alexcarpenter
body: Folks will need a way to target the background color when in the checked state. There is some precedence for `applyDataStateProps` in some of the components that have states that allow users to target.
created_at: 2025-05-04T23:51:41Z
author_association: MEMBER
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: LekoArts
body: This component isn't accessible as it could be and it's not following best practices. https://www.w3.org/WAI/ARIA/apg/patterns/switch/ shows three variants you could follow. https://inclusive-components.design/toggle-button/#conclusion has a lengthier explanation for that.

I'd probably follow the markup and functionality that https://react-spectrum.adobe.com/react-aria/Switch.html provides
created_at: 2025-05-05T01:38:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -175,16 +184,53 @@ function Card(props: CardProps) {
             />
           </Box>
         ) : null}
+
+        {showStatusRow && (
+          <Box
+            sx={t => ({
+              padding: t.space.$1,
+              borderTopWidth: t.borderWidths.$normal,
+              borderTopStyle: t.borderStyles.$solid,
+              borderTopColor: t.colors.$neutralAlpha100,
+            })}
+          >
+            {subscription?.status === 'active' || (isImplicitlyActiveOrUpcoming && subscriptions.length === 0) ? (
+              <Text
+                elementDescriptor={descriptors.pricingTableCardBadge}
+                variant='caption'
+                colorScheme='body'
+                localizationKey={localizationKeys('badge__currentPlan')}
+                sx={{ textAlign: 'center' }}
+              />
+            ) : (
+              <Text
+                elementDescriptor={descriptors.pricingTableCardBadge}
+                variant='caption'
+                colorScheme='body'
+                localizationKey={
+                  subscription
+                    ? localizationKeys('badge__startsAt', {
+                        date: subscription.periodStart,
+                      })
+                    : localizationKeys('badge__upcomingPlan')
+                }
+                sx={{ textAlign: 'center' }}
+              />
+            )}
+          </Box>
+        )}
+
         {(!plan.isDefault || !isDefaultPlanImplicitlyActiveOrUpcoming) && (
           <Box
             elementDescriptor={descriptors.pricingTableCardAction}
             sx={t => ({
               marginTop: 'auto',
               padding: isCompact ? t.space.$3 : t.space.$4,
-              borderTopWidth: hasFeatures ? t.borderWidths.$normal : 0,
+              borderTopWidth: t.borderWidths.$normal,
               borderTopStyle: t.borderStyles.$solid,
               borderTopColor: t.colors.$neutralAlpha100,
               background: undefined,
+              order: ctaPosition === 'top' ? -1 : undefined,
path: packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
user: login: aeliox
body: no because i added the "badge" replacement in this element, so a straight reversal of the children won't work
created_at: 2025-05-05T02:50:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -175,16 +184,53 @@ function Card(props: CardProps) {
             />
           </Box>
         ) : null}
+
+        {showStatusRow && (
path: packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
user: login: aeliox
body: this really just boils down to waiting until _both_ plans _and_ subscriptions resolve before rendering
created_at: 2025-05-05T02:51:35Z
author_association: CONTRIBUTOR
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: aeliox
body: i'm definitely sprinting to get a new element in place before we launch this – i'd be very appreciative of assistance in making sure this element follows best practices!
created_at: 2025-05-05T02:53:18Z
author_association: CONTRIBUTOR
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: LekoArts
body: Sure, I can give you an example on another branch 👍 
created_at: 2025-05-05T03:08:21Z
author_association: CONTRIBUTOR
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: LekoArts
body: Here's an example: https://github.com/clerk/javascript/compare/keiran/pricing-card-layouts...temp/pr-5824-fixes
IMO the `label` needs to be required, otherwise it's not an accessible switch.

You can check if the `<label>` is focused and only then change the focus style of the indicator
created_at: 2025-05-05T04:08:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,7 +72,7 @@ export const PlansContextProvider = ({ children }: PropsWithChildren) => {
     <PlansContext.Provider
       value={{
         componentName: 'Plans',
-        plans: plans || [],
+        plans: plans && subscriptions ? plans : [],
path: packages/clerk-js/src/ui/contexts/components/Plans.tsx
user: login: octoper
body: This breaks the E2E tests when the user is signed out, also I'm still not sure why it was added
created_at: 2025-05-05T08:37:50Z
author_association: MEMBER
diff_hunk: @@ -175,16 +184,53 @@ function Card(props: CardProps) {
             />
           </Box>
         ) : null}
+
+        {showStatusRow && (
+          <Box
+            sx={t => ({
+              padding: t.space.$1,
+              borderTopWidth: t.borderWidths.$normal,
+              borderTopStyle: t.borderStyles.$solid,
+              borderTopColor: t.colors.$neutralAlpha100,
+            })}
+          >
+            {subscription?.status === 'active' || (isImplicitlyActiveOrUpcoming && subscriptions.length === 0) ? (
path: packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
user: login: octoper
body: This also feels weird that this is not gated behind `isSignedIn`, not sure why a not signed in user need to see the current plan
created_at: 2025-05-05T09:41:09Z
author_association: MEMBER
diff_hunk: @@ -72,7 +72,7 @@ export const PlansContextProvider = ({ children }: PropsWithChildren) => {
     <PlansContext.Provider
       value={{
         componentName: 'Plans',
-        plans: plans || [],
+        plans: plans && subscriptions ? plans : [],
path: packages/clerk-js/src/ui/contexts/components/Plans.tsx
user: login: octoper
body: This has been fixed now
created_at: 2025-05-05T13:11:09Z
author_association: MEMBER
diff_hunk: @@ -175,16 +184,53 @@ function Card(props: CardProps) {
             />
           </Box>
         ) : null}
+
+        {showStatusRow && (
+          <Box
+            sx={t => ({
+              padding: t.space.$1,
+              borderTopWidth: t.borderWidths.$normal,
+              borderTopStyle: t.borderStyles.$solid,
+              borderTopColor: t.colors.$neutralAlpha100,
+            })}
+          >
+            {subscription?.status === 'active' || (isImplicitlyActiveOrUpcoming && subscriptions.length === 0) ? (
path: packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx
user: login: octoper
body: I've added a check to only showing the status row if the user is signed in
created_at: 2025-05-05T13:11:47Z
author_association: MEMBER
path: packages/clerk-js/src/ui/elements/Switch.tsx
user: login: alexcarpenter
body: @LekoArts https://github.com/clerk/javascript/pull/5830
created_at: 2025-05-05T13:23:11Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-04T22:24:51Z
body: ###  🦋  Changeset detected

Latest commit: de54484064ba16e02bac22ff0070fe576c3b14c8

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 22 packages</summary>

  | Name                        | Type  |
| --------------------------- | ----- |
| @clerk/localizations        | Patch |
| @clerk/clerk-js             | Patch |
| @clerk/types                | Patch |
| @clerk/clerk-react          | Patch |
| @clerk/chrome-extension     | Patch |
| @clerk/clerk-expo           | Patch |
| @clerk/agent-toolkit        | Patch |
| @clerk/astro                | Patch |
| @clerk/backend              | Patch |
| @clerk/elements             | Patch |
| @clerk/expo-passkeys        | Patch |
| @clerk/express              | Patch |
| @clerk/fastify              | Patch |
| @clerk/nextjs               | Patch |
| @clerk/nuxt                 | Patch |
| @clerk/react-router         | Patch |
| @clerk/remix                | Patch |
| @clerk/shared               | Patch |
| @clerk/tanstack-react-start | Patch |
| @clerk/testing              | Patch |
| @clerk/themes               | Patch |
| @clerk/vue                  | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/clerk/javascript/new/keiran/pricing-card-layouts?filename=.changeset/purple-forks-march.md&value=---%0A%22%40clerk%2Fclerk-js%22%3A%20patch%0A%22%40clerk%2Flocalizations%22%3A%20patch%0A%22%40clerk%2Ftesting%22%3A%20patch%0A%22%40clerk%2Ftypes%22%3A%20patch%0A---%0A%0Achore(clerk-js%2Ctypes%2Clocalizations)%3A%20New%20layout%20for%20PricingTable%20plan%20cards%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-05-04T22:24:52Z
body: [vc]: #evKPf3341U5IJM3rltGRWtrx/79fGA/BhPvdU0cpY7s=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjbGVyay1qcy1zYW5kYm94IiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImNsZXJrLWpzLXNhbmRib3gtZ2l0LWtlaXJhbi1wcmljaW5nLWNhcmQtbGF5b3V0cy5jbGVya3N0YWdlLmRldiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vY2xlcmstcHJvZHVjdGlvbi9jbGVyay1qcy1zYW5kYm94LzM0ODlLa29DaTZTWUYzREtWejJYeldleWN2WGIiLCJwcmV2aWV3VXJsIjoiY2xlcmstanMtc2FuZGJveC1naXQta2VpcmFuLXByaWNpbmctY2FyZC1sYXlvdXRzLmNsZXJrc3RhZ2UuZGV2IiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwicm9vdERpcmVjdG9yeSI6InBhY2thZ2VzL2NsZXJrLWpzIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **clerk-js-sandbox** | ✅ Ready ([Inspect](https://vercel.com/clerk-production/clerk-js-sandbox/3489KkoCi6SYF3DKVz2XzWeycvXb)) | [Visit Preview](https://clerk-js-sandbox-git-keiran-pricing-card-layouts.clerkstage.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/clerk-js-sandbox-git-keiran-pricing-card-layouts.clerkstage.dev?via=pr-comment-feedback-link) | May 5, 2025 3:39pm |


author_association: NONE