Repository: clerk/javascript
Pull request #5337: feat(clerk-js, types): `<Drawer />` component
Author: login: alexcarpenter
Labels: name: types
name: clerk-js
Created: 2025-03-12T15:41:44Z | Merged: 2025-03-14T17:58:02Z

## Description

Introduces `<Drawer />` component to be used within Commerce components.

### Drawer

The drawer component is a modal that enters from the right of the screen. Currently when open, we disallow interactions with elements outside of the drawer by using the `outsideElementsInert` prop.

#### Default

By default the `<Drawer />` uses fixed positioning which will render to fill the viewport height and enter from the right.

```tsx
function DefaultExample() {
  const [isOpen, setIsOpen] = React.useState()
  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open</button>
      <Drawer.Root
        open={isOpen}
        onOpenChange={setIsOpen}
      >
        <Drawer.Overlay />
        <Drawer.Content>
          <Drawer.Header title='Checkout' />
          <Drawer.Body>
            ...
          </Drawer.Body>
          <Drawer.Footer>
            <button>Continue</button>
          </Drawer.Footer>
        </Drawer.Content>
      </Drawer.Root>
    </>
  );
});
```

https://github.com/user-attachments/assets/1a2565b8-0258-42d8-af98-467904fa4554

#### Within a modal

There are instances where we need to render a drawer within a modal. To do that, we can make use of the `strategy` and `portalProps` options.

```tsx
function ModalExample() {
  const [isOpen, setIsOpen] = React.useState()
  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open</button>
      <Drawer.Root
        open={isOpen}
        onOpenChange={setIsOpen}
        // Absolutely position the drawer within an element
        strategy='absolute'
        portalProps={{
         // Portal the component into an element with the following id
         id: PROFILE_CARD_SCROLLBOX_ID
       }}
      >
        <Drawer.Overlay />
        <Drawer.Content>
          <Drawer.Header title='Checkout' />
          <Drawer.Body>
            ...
          </Drawer.Body>
          <Drawer.Footer>
            <button>Continue</button>
          </Drawer.Footer>
        </Drawer.Content>
      </Drawer.Root>
    </>
  );
});
```


https://github.com/user-attachments/assets/2793b8b7-39c6-4c67-9b40-fdc926c3398d


<!-- Fixes #(issue number) -->

## 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
- [x] 🌟 New feature
- [ ] 🔨 Breaking change
- [ ] 📖 Refactoring / dependency upgrade / documentation
- [ ] other:



Diff:
diff --git a/.changeset/lucky-cobras-cough.md b/.changeset/lucky-cobras-cough.md
new file mode 100644
index 00000000000..b4bc1768757
--- /dev/null
+++ b/.changeset/lucky-cobras-cough.md
@@ -0,0 +1,6 @@
+---
+'@clerk/clerk-js': patch
+'@clerk/types': patch
+---
+
+Introduce `<Drawer />` component and update commerce components implementations to make use of it.
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index 338aceb7db7..50d50a6ce75 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -1,10 +1,10 @@
 {
   "files": [
-    { "path": "./dist/clerk.js", "maxSize": "573kB" },
+    { "path": "./dist/clerk.js", "maxSize": "575kB" },
     { "path": "./dist/clerk.browser.js", "maxSize": "78kB" },
     { "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
-    { "path": "./dist/ui-common*.js", "maxSize": "92KB" },
-    { "path": "./dist/vendors*.js", "maxSize": "28.5KB" },
+    { "path": "./dist/ui-common*.js", "maxSize": "93KB" },
+    { "path": "./dist/vendors*.js", "maxSize": "30KB" },
     { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
     { "path": "./dist/createorganization*.js", "maxSize": "5KB" },
     { "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },
diff --git a/packages/clerk-js/src/ui/common/CommerceBlade.tsx b/packages/clerk-js/src/ui/common/CommerceBlade.tsx
deleted file mode 100644
index 360e9a55b09..00000000000
--- a/packages/clerk-js/src/ui/common/CommerceBlade.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import { Box } from '../customizables';
-import { animations } from '../styledSystem';
-
-interface CommerceBladeProps {
-  isOpen: boolean;
-  isFullscreen?: boolean;
-  children: React.ReactNode;
-}
-
-export const CommerceBlade = ({ isOpen, isFullscreen, children }: CommerceBladeProps) => {
-  const [mounted, setMounted] = useState(false);
-
-  useEffect(() => {
-    if (isOpen) {
-      setMounted(true);
-      return;
-    } else {
-      const timer = setTimeout(() => {
-        setMounted(false);
-      }, 280);
-      return () => clearTimeout(timer);
-    }
-  }, [isOpen, mounted]);
-
-  if (!mounted && !isOpen) {
-    return null;
-  }
-
-  return (
-    <CommerceBladeContent
-      isOpen={isOpen}
-      isFullscreen={isFullscreen}
-    >
-      {children}
-    </CommerceBladeContent>
-  );
-};
-
-const CommerceBladeContent = ({ isOpen, isFullscreen, children }: CommerceBladeProps) => {
-  return (
-    <>
-      <Box
-        sx={t => ({
-          position: 'absolute',
-          zIndex: t.zIndices.$modal,
-          inset: 0,
-          backgroundColor: t.colors.$whiteAlpha300,
-          animation: `${isOpen ? animations.fadeIn : animations.fadeOut} ${t.transitionDuration.$slower} ${t.transitionTiming.$common}`,
-        })}
-      />
-      <Box
-        sx={t => ({
-          position: isFullscreen ? 'fixed' : 'absolute',
-          width: t.sizes.$100,
-          inset: isFullscreen ? t.space.$3 : 0,
-          insetInlineStart: 'auto',
-          overflow: 'hidden',
-          backgroundColor: t.colors.$colorBackground,
-          borderRadius: `${t.radii.$xl} ${isFullscreen ? t.radii.$xl : 0} ${isFullscreen ? t.radii.$xl : 0} ${t.radii.$xl}`,
-          boxShadow:
-            '0px 0px 0px 1px rgba(25, 28, 33, 0.06), 0px 15px 35px -5px rgba(25, 28, 33, 0.20), 0px 5px 15px 0px rgba(0, 0, 0, 0.08)',
-          zIndex: t.zIndices.$modal,
-          animation: `${isOpen ? animations.drawerSlideIn : animations.drawerSlideOut} ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`,
-        })}
-      >
-        {children}
-      </Box>
-    </>
-  );
-};
diff --git a/packages/clerk-js/src/ui/common/index.ts b/packages/clerk-js/src/ui/common/index.ts
index 4641ea459bf..bea7cf9e442 100644
--- a/packages/clerk-js/src/ui/common/index.ts
+++ b/packages/clerk-js/src/ui/common/index.ts
@@ -1,6 +1,5 @@
 export * from './BlockButtons';
 export * from './CalloutWithAction';
-export * from './CommerceBlade';
 export * from './constants';
 export * from './EmailLinkStatusCard';
 export * from './EmailLinkVerify';
diff --git a/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx b/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx
index 81ad97b6828..848a2edc0da 100644
--- a/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx
+++ b/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx
@@ -1,8 +1,9 @@
 import type { __experimental_CheckoutProps } from '@clerk/types';
 
-import { CommerceBlade } from '../../common';
+import { PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
 import { useCheckoutContext, withCoreUserGuard } from '../../contexts';
 import { Flow } from '../../customizables';
+import { Drawer } from '../../elements';
 import { Route, Switch } from '../../router';
 import { CheckoutPage } from './CheckoutPage';
 
@@ -21,14 +22,24 @@ export const __experimental_Checkout = (props: __experimental_CheckoutProps) =>
 };
 
 const AuthenticatedRoutes = withCoreUserGuard((props: __experimental_CheckoutProps) => {
-  const { mode = 'mounted', isShowingBlade = false } = useCheckoutContext();
+  const { mode = 'mounted', isOpen = false, setIsOpen = () => {} } = useCheckoutContext();
 
   return (
-    <CommerceBlade
-      isOpen={isShowingBlade}
-      isFullscreen={mode === 'mounted'}
+    <Drawer.Root
+      open={isOpen}
+      onOpenChange={setIsOpen}
+      strategy={mode === 'mounted' ? 'fixed' : 'absolute'}
+      portalProps={{
+        id: mode === 'modal' ? PROFILE_CARD_SCROLLBOX_ID : undefined,
+      }}
     >
-      <CheckoutPage {...props} />
-    </CommerceBlade>
+      <Drawer.Overlay />
+      <Drawer.Content>
+        <Drawer.Header title='Checkout' />
+        <Drawer.Body>
+          <CheckoutPage {...props} />
+        </Drawer.Body>
+      </Drawer.Content>
+    </Drawer.Root>
   );
 });
diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx
index 13a43e04fba..00b9d5cb7ab 100644
--- a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx
+++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx
@@ -4,34 +4,30 @@ import { useCheckoutContext } from '../../contexts';
 import { Box, Button, Col, Flex, Icon, Text } from '../../customizables';
 import { LineItems } from '../../elements';
 import { Check } from '../../icons';
-import type { ThemableCssProp } from '../../styledSystem';
 
-export const CheckoutComplete = ({
-  checkout,
-  sx,
-}: {
-  checkout: __experimental_CommerceCheckoutResource;
-  sx?: ThemableCssProp;
-}) => {
-  const { handleCloseBlade = () => {} } = useCheckoutContext();
+export const CheckoutComplete = ({ checkout }: { checkout: __experimental_CommerceCheckoutResource }) => {
+  const { setIsOpen } = useCheckoutContext();
+
+  const handleClose = () => {
+    if (setIsOpen) {
+      setIsOpen(false);
+    }
+  };
 
   return (
     <Col
-      sx={[
-        t => ({
-          width: '100%',
-          padding: t.space.$4,
-        }),
-        sx,
-      ]}
+      sx={{
+        flex: 1,
+      }}
     >
       <Col
         align='center'
         justify='center'
         gap={8}
-        sx={{
+        sx={t => ({
           flex: 1,
-        }}
+          paddingBlock: t.space.$4,
+        })}
       >
         <SuccessCircle />
 
@@ -48,14 +44,15 @@ export const CheckoutComplete = ({
           </Text>
         </Col>
       </Col>
+
       <Col
         gap={2}
         sx={t => ({
-          flex: 0,
-          paddingTop: t.space.$4,
+          padding: t.space.$4,
           borderTopWidth: t.borderWidths.$normal,
           borderTopStyle: t.borderStyles.$solid,
           borderTopColor: t.colors.$neutralAlpha100,
+          position: 'relative',
         })}
       >
         <LineItems.Root>
@@ -90,7 +87,7 @@ export const CheckoutComplete = ({
             width: '100%',
             marginTop: t.space.$2,
           })}
-          onClick={handleCloseBlade}
+          onClick={handleClose}
         >
           {/* TODO(@COMMERCE): needs localization */}
           Continue
diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx
index e1087cc7e22..f44fb7666f6 100644
--- a/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx
+++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx
@@ -8,11 +8,10 @@ import type { Stripe } from '@stripe/stripe-js';
 import { loadStripe } from '@stripe/stripe-js';
 import { useEffect, useRef, useState } from 'react';
 
-import { useCheckoutContext, useEnvironment } from '../../contexts';
-import { Alert, Box, Button, Col, Flex, Heading, Icon, Spinner } from '../../customizables';
+import { useEnvironment } from '../../contexts';
+import { Alert, Col, Flex, Spinner } from '../../customizables';
 import { LineItems } from '../../elements';
 import { useCheckout } from '../../hooks';
-import { Close } from '../../icons';
 import { CheckoutComplete } from './CheckoutComplete';
 import { CheckoutForm } from './CheckoutForm';
 
@@ -44,8 +43,6 @@ export const CheckoutPage = (props: __experimental_CheckoutProps) => {
 
   return (
     <>
-      <CheckoutHeader title='Checkout' />
-
       {isLoading ? (
         <Flex
           align='center'
@@ -64,24 +61,13 @@ export const CheckoutPage = (props: __experimental_CheckoutProps) => {
           <Alert colorScheme='danger'>There was a problem, please try again later.</Alert>
         </Flex>
       ) : checkout.status === 'completed' ? (
-        <CheckoutComplete
-          checkout={checkout}
-          sx={t => ({ height: `calc(100% - ${t.space.$12})` })}
-        />
+        <CheckoutComplete checkout={checkout} />
       ) : (
-        <Box
-          sx={t => ({
-            overflowY: 'auto',
-            /* minus the height of the header */
-            height: `calc(100% - ${t.space.$12})`,
-            overflowX: 'hidden',
-          })}
-        >
+        <>
           <Col
             gap={3}
             sx={t => ({
               padding: t.space.$4,
-              backgroundColor: t.colors.$neutralAlpha25,
               borderBottomWidth: t.borderWidths.$normal,
               borderBottomStyle: t.borderStyles.$solid,
               borderBottomColor: t.colors.$neutralAlpha100,
@@ -105,49 +91,12 @@ export const CheckoutPage = (props: __experimental_CheckoutProps) => {
               />
             </Elements>
           )}
-        </Box>
+        </>
       )}
     </>
   );
 };
 
-const CheckoutHeader = ({ title }: { title: string }) => {
-  const { handleCloseBlade = () => {} } = useCheckoutContext();
-
-  return (
-    <Flex
-      align='center'
-      justify='between'
-      gap={2}
-      sx={t => ({
-        position: 'sticky',
-        top: 0,
-        width: '100%',
-        height: t.space.$12,
-        paddingInline: `${t.space.$5} ${t.space.$2}`,
-        borderBottomWidth: t.borderWidths.$normal,
-        borderBottomStyle: t.borderStyles.$solid,
-        borderBottomColor: t.colors.$neutralAlpha100,
-      })}
-    >
-      <Heading textVariant='h2'>{title}</Heading>
-      <Button
-        variant='ghost'
-        onClick={handleCloseBlade}
-        sx={t => ({
-          color: t.colors.$neutralAlpha400,
-          padding: t.space.$2,
-        })}
-      >
-        <Icon
-          icon={Close}
-          size='md'
-        />
-      </Button>
-    </Flex>
-  );
-};
-
 // TODO(@COMMERCE): needs localization
 const CheckoutPlanRows = ({
   plan,
diff --git a/packages/clerk-js/src/ui/components/PricingTable/PlanCard.tsx b/packages/clerk-js/src/ui/components/PricingTable/PlanCard.tsx
index f786d447c38..bc5aa512e21 100644
--- a/packages/clerk-js/src/ui/components/PricingTable/PlanCard.tsx
+++ b/packages/clerk-js/src/ui/components/PricingTable/PlanCard.tsx
@@ -23,10 +23,16 @@ import type { ThemableCssProp } from '../../styledSystem';
 import { common } from '../../styledSystem';
 import { colors } from '../../utils';
 
+export type PlanPeriod = 'month' | 'annual';
+
+/* -------------------------------------------------------------------------------------------------
+ * PlanCard
+ * -----------------------------------------------------------------------------------------------*/
+
 interface PlanCardProps {
   plan: __experimental_CommercePlanResource;
-  period: string;
-  setPeriod: (k: string) => void;
+  planPeriod: PlanPeriod;
+  setPlanPeriod: (p: PlanPeriod) => void;
   onSelect: (plan: __experimental_CommercePlanResource) => void;
   isCompact?: boolean;
   props: __experimental_PricingTableProps;
@@ -34,37 +40,11 @@ interface PlanCardProps {
 
 export function PlanCard(props: PlanCardProps) {
   const clerk = useClerk();
-  const { plan, period, setPeriod, onSelect, props: pricingTableProps, isCompact = false } = props;
-  const {
-    id,
-    slug,
-    name,
-    description,
-    avatarUrl,
-    features,
-    isActiveForPayer,
-    hasBaseFee,
-    currencySymbol,
-    amountFormatted,
-    annualMonthlyAmountFormatted,
-  } = plan;
+  const { plan, planPeriod, setPlanPeriod, onSelect, props: pricingTableProps, isCompact = false } = props;
   const { ctaPosition = 'top', collapseFeatures = false } = pricingTableProps;
-  const [showAllFeatures, setShowAllFeatures] = React.useState(false);
+  const { id, slug, isActiveForPayer, features } = plan;
   const totalFeatures = features.length;
   const hasFeatures = totalFeatures > 0;
-  const canToggleFeatures = isCompact && totalFeatures > 3;
-  const prefersReducedMotion = usePrefersReducedMotion();
-  const { animations: appearanceAnimations } = useAppearance().parsedLayout;
-  const planCardFeePeriodNoticeAnimation: ThemableCssProp = t => ({
-    transition:
-      appearanceAnimations && !prefersReducedMotion
-        ? `grid-template-rows ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`
-        : 'none',
-  });
-
-  const toggleFeatures = () => {
-    setShowAllFeatures(prev => !prev);
-  };
 
   return (
     <Box
@@ -87,159 +67,13 @@ export function PlanCard(props: PlanCardProps) {
         textAlign: 'left',
       })}
     >
-      <Box
-        elementDescriptor={descriptors.planCardHeader}
-        sx={t => ({
-          padding: isCompact ? t.space.$3 : t.space.$4,
-        })}
-      >
-        {avatarUrl || isActiveForPayer ? (
-          <Box
-            elementDescriptor={descriptors.planCardAvatarBadgeContainer}
-            sx={t => ({
-              display: 'flex',
-              alignItems: 'start',
-              justifyContent: 'space-between',
-              gap: t.space.$3,
-              marginBlockEnd: t.space.$3,
-            })}
-          >
-            {avatarUrl ? (
-              <Avatar
-                boxElementDescriptor={descriptors.planCardAvatar}
-                size={_ => 40}
-                title={name}
-                initials={name[0]}
-                rounded={false}
-                imageUrl={avatarUrl}
-              />
-            ) : null}
-            {isActiveForPayer ? (
-              <Badge
-                localizationKey={localizationKeys('badge__currentPlan')}
-                colorScheme='secondary'
-              />
-            ) : null}
-          </Box>
-        ) : null}
-        <Heading
-          elementDescriptor={descriptors.planCardTitle}
-          as='h2'
-          textVariant={isCompact ? 'h3' : 'h2'}
-        >
-          {name}
-        </Heading>
-        {!isCompact && description ? (
-          <Text
-            elementDescriptor={descriptors.planCardDescription}
-            variant='subtitle'
-            colorScheme='secondary'
-          >
-            {description}
-          </Text>
-        ) : null}
-        <Flex
-          elementDescriptor={descriptors.planCardFeeContainer}
-          align='center'
-          wrap='wrap'
-          sx={t => ({
-            marginTop: isCompact ? t.space.$2 : t.space.$3,
-            columnGap: t.space.$1x5,
-          })}
-        >
-          {hasBaseFee ? (
-            <>
-              <Text
-                elementDescriptor={descriptors.planCardFee}
-                variant={isCompact ? 'h2' : 'h1'}
-                colorScheme='body'
-              >
-                {currencySymbol}
-                {period === 'month' ? amountFormatted : annualMonthlyAmountFormatted}
-              </Text>
-              <Text
-                elementDescriptor={descriptors.planCardFeePeriod}
-                variant='caption'
-                colorScheme='secondary'
-                sx={t => ({
-                  textTransform: 'lowercase',
-                  ':before': {
-                    content: '"/"',
-                    marginInlineEnd: t.space.$1,
-                  },
-                })}
-                localizationKey={localizationKeys('__experimental_commerce.month')}
-              />
-              <Box
-                elementDescriptor={descriptors.planCardFeePeriodNotice}
-                sx={[
-                  _ => ({
-                    width: '100%',
-                    display: 'grid',
-                    gridTemplateRows: period === 'annual' ? '1fr' : '0fr',
-                  }),
-                  planCardFeePeriodNoticeAnimation,
-                ]}
-                // @ts-ignore - Needed until React 19 support
-                inert={period !== 'annual' ? 'true' : undefined}
-              >
-                <Box
-                  elementDescriptor={descriptors.planCardFeePeriodNoticeInner}
-                  sx={{
-                    overflow: 'hidden',
-                    minHeight: 0,
-                  }}
-                >
-                  <Text
-                    elementDescriptor={descriptors.planCardFeePeriodNoticeLabel}
-                    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>
-            </>
-          ) : (
-            <Text
-              elementDescriptor={descriptors.planCardFee}
-              variant={isCompact ? 'h2' : 'h1'}
-              localizationKey={localizationKeys('__experimental_commerce.free')}
-              colorScheme='body'
-            />
-          )}
-        </Flex>
-        {hasBaseFee ? (
-          <Box
-            elementDescriptor={descriptors.planCardPeriodToggle}
-            sx={t => ({
-              display: 'flex',
-              marginTop: t.space.$3,
-            })}
-          >
-            <SegmentedControl.Root
-              aria-label='Set pay period'
-              value={period}
-              onChange={setPeriod}
-            >
-              <SegmentedControl.Button value='month'>Monthly</SegmentedControl.Button>
-              <SegmentedControl.Button value='annual'>Annually</SegmentedControl.Button>
-            </SegmentedControl.Root>
-          </Box>
-        ) : null}
-      </Box>
+      <PlanCardHeader
+        plan={plan}
+        isCompact={isCompact}
+        isActivePlan={isActiveForPayer}
+        planPeriod={planPeriod}
+        setPlanPeriod={setPlanPeriod}
+      />
       <ReversibleContainer reverse={ctaPosition === 'top'}>
         {!collapseFeatures && hasFeatures ? (
           <Box
@@ -255,56 +89,10 @@ export function PlanCard(props: PlanCardProps) {
               borderTopColor: t.colors.$neutralAlpha100,
             })}
           >
-            <Box
-              elementDescriptor={descriptors.planCardFeaturesList}
-              as='ul'
-              role='list'
-              sx={t => ({
-                display: 'grid',
-                flex: '1',
-                rowGap: isCompact ? t.space.$2 : t.space.$3,
-              })}
-            >
-              {features.slice(0, showAllFeatures ? totalFeatures : 3).map(feature => (
-                <Box
-                  elementDescriptor={descriptors.planCardFeaturesListItem}
-                  elementId={descriptors.planCardFeaturesListItem.setId(feature.slug)}
-                  key={feature.id}
-                  as='li'
-                >
-                  <Flex
-                    gap={2}
-                    align='baseline'
-                  >
-                    <Icon
-                      icon={Check}
-                      colorScheme='neutral'
-                      size='sm'
-                      aria-hidden
-                    />
-                    <Text colorScheme='body'>{feature.description || feature.name}</Text>
-                  </Flex>
-                </Box>
-              ))}
-            </Box>
-            {canToggleFeatures && (
-              <SimpleButton
-                onClick={toggleFeatures}
-                variant='link'
-                sx={t => ({
-                  marginBlockStart: t.space.$2,
-                  gap: t.space.$1,
-                })}
-              >
-                <Icon
-                  icon={showAllFeatures ? Minus : Plus}
-                  colorScheme='neutral'
-                  size='md'
-                  aria-hidden
-                />
-                {showAllFeatures ? 'Hide features' : 'See all features'}
-              </SimpleButton>
-            )}
+            <PlanCardFeaturesList
+              plan={plan}
+              isCompact={isCompact}
+            />
           </Box>
         ) : null}
         <Box
@@ -342,6 +130,298 @@ export function PlanCard(props: PlanCardProps) {
   );
 }
 
+/* -------------------------------------------------------------------------------------------------
+ * PlanCardHeader
+ * -----------------------------------------------------------------------------------------------*/
+
+interface PlanCardHeaderProps {
+  plan: __experimental_CommercePlanResource;
+  isCompact?: boolean;
+  isActivePlan?: boolean;
+  planPeriod: PlanPeriod;
+  setPlanPeriod: (val: PlanPeriod) => void;
+  closeSlot?: React.ReactNode;
+}
+
+export const PlanCardHeader = React.forwardRef<HTMLDivElement, PlanCardHeaderProps>((props, ref) => {
+  const prefersReducedMotion = usePrefersReducedMotion();
+  const { animations: layoutAnimations } = useAppearance().parsedLayout;
+  const { plan, isCompact, planPeriod, setPlanPeriod, closeSlot } = props;
+  const { name, avatarUrl, isActiveForPayer } = plan;
+  const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
+  const planCardFeePeriodNoticeAnimation: ThemableCssProp = t => ({
+    transition: isMotionSafe
+      ? `grid-template-rows ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`
+      : 'none',
+  });
+  return (
+    <Box
+      ref={ref}
+      elementDescriptor={descriptors.planCardHeader}
+      sx={t => ({
+        width: '100%',
+        padding: isCompact ? t.space.$3 : t.space.$4,
+      })}
+    >
+      {avatarUrl || isActiveForPayer || closeSlot ? (
+        <Box
+          elementDescriptor={descriptors.planCardAvatarBadgeContainer}
+          sx={t => ({
+            marginBlockEnd: t.space.$3,
+            ...(!avatarUrl && !isActiveForPayer
+              ? {
+                  float: 'right',
+                }
+              : {
+                  display: 'grid',
+                  gridTemplateColumns: 'repeat(2, minmax(0,1fr))',
+                  alignItems: 'start',
+                  justifyContent: 'space-between',
+                  gap: t.space.$3,
+                }),
+          })}
+        >
+          {avatarUrl ? (
+            <Avatar
+              boxElementDescriptor={descriptors.planCardAvatar}
+              size={_ => 40}
+              title={name}
+              initials={name[0]}
+              rounded={false}
+              imageUrl={avatarUrl}
+              sx={{
+                gridRowStart: 1,
+              }}
+            />
+          ) : null}
+          <ReversibleContainer reverse={avatarUrl && closeSlot ? true : false}>
+            {isActiveForPayer ? (
+              <Badge
+                localizationKey={localizationKeys('badge__currentPlan')}
+                colorScheme='secondary'
+                sx={
+                  !avatarUrl
+                    ? {
+                        justifySelf: 'start',
+                      }
+                    : {
+                        justifySelf: avatarUrl && closeSlot ? 'start' : 'end',
+                      }
+                }
+              />
+            ) : null}
+            {closeSlot}
+          </ReversibleContainer>
+        </Box>
+      ) : null}
+      <Heading
+        elementDescriptor={descriptors.planCardTitle}
+        as='h2'
+        textVariant={isCompact ? 'h3' : 'h2'}
+      >
+        {plan.name}
+      </Heading>
+      {!isCompact && plan.description ? (
+        <Text
+          elementDescriptor={descriptors.planCardDescription}
+          variant='subtitle'
+          colorScheme='secondary'
+        >
+          {plan.description}
+        </Text>
+      ) : null}
+      <Flex
+        elementDescriptor={descriptors.planCardFeeContainer}
+        align='center'
+        wrap='wrap'
+        sx={t => ({
+          marginTop: isCompact ? t.space.$2 : t.space.$3,
+          columnGap: t.space.$1x5,
+        })}
+      >
+        {plan.hasBaseFee ? (
+          <>
+            <Text
+              elementDescriptor={descriptors.planCardFee}
+              variant={isCompact ? 'h2' : 'h1'}
+              colorScheme='body'
+            >
+              {plan.currencySymbol}
+              {planPeriod === 'month' ? plan.amountFormatted : plan.annualMonthlyAmountFormatted}
+            </Text>
+            <Text
+              elementDescriptor={descriptors.planCardFeePeriod}
+              variant='caption'
+              colorScheme='secondary'
+              sx={t => ({
+                textTransform: 'lowercase',
+                ':before': {
+                  content: '"/"',
+                  marginInlineEnd: t.space.$1,
+                },
+              })}
+              localizationKey={localizationKeys('__experimental_commerce.month')}
+            />
+            <Box
+              elementDescriptor={descriptors.planCardFeePeriodNotice}
+              sx={[
+                _ => ({
+                  width: '100%',
+                  display: 'grid',
+                  gridTemplateRows: planPeriod === 'annual' ? '1fr' : '0fr',
+                }),
+                planCardFeePeriodNoticeAnimation,
+              ]}
+              // @ts-ignore - Needed until React 19 support
+              inert={planPeriod !== 'annual' ? 'true' : undefined}
+            >
+              <Box
+                elementDescriptor={descriptors.planCardFeePeriodNoticeInner}
+                sx={{
+                  overflow: 'hidden',
+                  minHeight: 0,
+                }}
+              >
+                <Text
+                  elementDescriptor={descriptors.planCardFeePeriodNoticeLabel}
+                  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>
+          </>
+        ) : (
+          <Text
+            elementDescriptor={descriptors.planCardFee}
+            variant={isCompact ? 'h2' : 'h1'}
+            localizationKey={localizationKeys('__experimental_commerce.free')}
+            colorScheme='body'
+          />
+        )}
+      </Flex>
+      {plan.hasBaseFee ? (
+        <Box
+          elementDescriptor={descriptors.planCardPeriodToggle}
+          sx={t => ({
+            display: 'flex',
+            marginTop: t.space.$3,
+          })}
+        >
+          <SegmentedControl.Root
+            aria-label='Set pay period'
+            value={planPeriod}
+            onChange={value => setPlanPeriod(value as PlanPeriod)}
+          >
+            <SegmentedControl.Button value='month'>Monthly</SegmentedControl.Button>
+            <SegmentedControl.Button value='annual'>Annually</SegmentedControl.Button>
+          </SegmentedControl.Root>
+        </Box>
+      ) : null}
+    </Box>
+  );
+});
+
+/* -------------------------------------------------------------------------------------------------
+ * PlanCardFeaturesList
+ * -----------------------------------------------------------------------------------------------*/
+
+interface PlanCardFeaturesListProps {
+  plan: __experimental_CommercePlanResource;
+  isCompact?: boolean;
+}
+
+export const PlanCardFeaturesList = React.forwardRef<HTMLDivElement, PlanCardFeaturesListProps>((props, ref) => {
+  const { plan, isCompact } = props;
+  const totalFeatures = plan.features.length;
+  const [showAllFeatures, setShowAllFeatures] = React.useState(false);
+  const canToggleFeatures = isCompact && totalFeatures > 3;
+  const toggleFeatures = () => {
+    setShowAllFeatures(prev => !prev);
+  };
+  return (
+    <Box
+      ref={ref}
+      elementDescriptor={descriptors.planCardFeaturesList}
+      elementId={isCompact ? descriptors.planCardFeaturesList.setId('compact') : undefined}
+      as='ul'
+      role='list'
+      sx={t => ({
+        display: 'grid',
+        flex: '1',
+        rowGap: isCompact ? t.space.$2 : t.space.$3,
+      })}
+    >
+      <Box
+        elementDescriptor={descriptors.planCardFeaturesList}
+        as='ul'
+        role='list'
+        sx={t => ({
+          display: 'grid',
+          flex: '1',
+          rowGap: isCompact ? t.space.$2 : t.space.$3,
+        })}
+      >
+        {plan.features.slice(0, showAllFeatures ? totalFeatures : 3).map(feature => (
+          <Box
+            elementDescriptor={descriptors.planCardFeaturesListItem}
+            elementId={descriptors.planCardFeaturesListItem.setId(feature.slug)}
+            key={feature.id}
+            as='li'
+            sx={t => ({
+              display: 'flex',
+              alignItems: 'baseline',
+              gap: t.space.$2,
+            })}
+          >
+            <Icon
+              icon={Check}
+              colorScheme='neutral'
+              size='sm'
+              aria-hidden
+              sx={t => ({
+                transform: `translateY(${t.space.$0x25})`,
+              })}
+            />
+            <Text colorScheme='body'>{feature.description || feature.name}</Text>
+          </Box>
+        ))}
+      </Box>
+      {canToggleFeatures && (
+        <SimpleButton
+          onClick={toggleFeatures}
+          variant='link'
+          sx={t => ({
+            marginBlockStart: t.space.$2,
+            gap: t.space.$1,
+          })}
+        >
+          <Icon
+            icon={showAllFeatures ? Minus : Plus}
+            colorScheme='neutral'
+            size='md'
+            aria-hidden
+          />
+          {showAllFeatures ? 'Hide features' : 'See all features'}
+        </SimpleButton>
+      )}
+    </Box>
+  );
+});
+
 function ReversibleContainer(props: React.PropsWithChildren<{ reverse?: boolean }>) {
   const { children, reverse } = props;
   return <>{reverse ? React.Children.toArray(children).reverse() : children}</>;
diff --git a/packages/clerk-js/src/ui/components/PricingTable/PlanDetailBlade.tsx b/packages/clerk-js/src/ui/components/PricingTable/PlanDetailBlade.tsx
deleted file mode 100644
index 3191ff06b05..00000000000
--- a/packages/clerk-js/src/ui/components/PricingTable/PlanDetailBlade.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import type { __experimental_CommercePlanResource } from '@clerk/types';
-import { useState } from 'react';
-
-import { CommerceBlade } from '../../common';
-import { Alert, Button, Col, Flex, Heading, Icon, localizationKeys, Text } from '../../customizables';
-import { Avatar } from '../../elements';
-import { Close } from '../../icons';
-
-interface PlanDetailBladeProps {
-  isOpen: boolean;
-  handleClose: () => void;
-  plan?: __experimental_CommercePlanResource;
-}
-
-export const PlanDetailBlade = ({ isOpen, handleClose, plan }: PlanDetailBladeProps) => {
-  if (!plan) {
-    return null;
-  }
-
-  return (
-    <CommerceBlade isOpen={isOpen}>
-      <Col
-        sx={{
-          height: '100%',
-        }}
-      >
-        <Col
-          gap={3}
-          sx={t => ({
-            flex: 0,
-            padding: t.space.$4,
-            borderBottomWidth: t.borderWidths.$normal,
-            borderBottomStyle: t.borderStyles.$solid,
-            borderBottomColor: t.colors.$neutralAlpha100,
-            backgroundImage: `linear-gradient(to top, ${t.colors.$neutralAlpha50} 30%, ${t.colors.$colorBackground} 100%)`,
-          })}
-        >
-          <Button
-            variant='ghost'
-            onClick={handleClose}
-            sx={t => ({
-              position: 'absolute',
-              top: t.space.$3,
-              right: t.space.$3,
-              color: t.colors.$neutralAlpha400,
-              padding: t.space.$2,
-            })}
-          >
-            <Icon
-              icon={Close}
-              size='md'
-            />
-          </Button>
-          <Avatar
-            size={_ => 40}
-            title={plan.name}
-            initials={plan.name[0]}
-            rounded={false}
-            imageUrl={plan.avatarUrl}
-          />
-          <Col>
-            <Heading textVariant='h1'>{plan.name}</Heading>
-            {plan.hasBaseFee ? (
-              <Flex
-                gap={2}
-                align='baseline'
-              >
-                <Text variant='subtitle'>
-                  {plan.currencySymbol}
-                  {plan.amountFormatted}
-                </Text>
-                <Flex
-                  gap={1}
-                  align='baseline'
-                >
-                  <Text
-                    variant='caption'
-                    colorScheme='secondary'
-                  >
-                    /
-                  </Text>
-                  <Text
-                    variant='caption'
-                    colorScheme='secondary'
-                    sx={{ textTransform: 'lowercase' }}
-                    localizationKey={localizationKeys('__experimental_commerce.month')}
-                  />
-                </Flex>
-              </Flex>
-            ) : (
-              <Text
-                variant='subtitle'
-                localizationKey={localizationKeys('__experimental_commerce.free')}
-              />
-            )}
-          </Col>
-          <Text
-            variant='body'
-            colorScheme='secondary'
-          >
-            {plan.description}
-          </Text>
-        </Col>
-
-        <Col
-          gap={6}
-          sx={t => ({
-            flex: 1,
-            padding: t.space.$4,
-            overflowY: 'auto',
-            overflowX: 'hidden',
-          })}
-        >
-          <Text
-            variant='caption'
-            colorScheme='secondary'
-          >
-            Available features
-          </Text>
-          {plan.features.map(feature => (
-            <Flex
-              key={feature.id}
-              gap={3}
-              align='start'
-            >
-              <Avatar
-                size={_ => 24}
-                title={feature.name}
-                rounded={false}
-                imageUrl={feature.avatarUrl}
-              />
-              <Col gap={1}>
-                <Text variant='buttonSmall'>{feature.name}</Text>
-                <Text
-                  variant='caption'
-                  colorScheme='secondary'
-                >
-                  {feature.description}
-                </Text>
-              </Col>
-            </Flex>
-          ))}
-        </Col>
-
-        <CancelFooter
-          plan={plan}
-          handleClose={handleClose}
-        />
-      </Col>
-    </CommerceBlade>
-  );
-};
-
-const CancelFooter = ({ plan }: { plan: __experimental_CommercePlanResource; handleClose: () => void }) => {
-  // const { __experimental_commerce } = useClerk();
-  const [showConfirmation, setShowConfirmation] = useState(false);
-  const [isSubmitting, setIsSubmitting] = useState(false);
-  const [hasError, setHasError] = useState(false);
-
-  const cancelSubscription = async () => {
-    setHasError(false);
-    setIsSubmitting(true);
-
-    // TODO: we need to get a handle on the subscription object in order to cancel it,
-    // but this method doesn't exist yet.
-    //
-    // await subscription.cancel().then(() => {
-    //   setIsSubmitting(false);
-    //   handleClose();
-    // }).catch(() => { setHasError(true); setIsSubmitting(false); });
-  };
-
-  // TODO: remove when we can hook up cancel button
-  // return null;
-
-  return (
-    <Col
-      gap={3}
-      sx={t => ({
-        flex: 0,
-        padding: t.space.$4,
-        borderTopWidth: t.borderWidths.$normal,
-        borderTopStyle: t.borderStyles.$solid,
-        borderTopColor: t.colors.$neutralAlpha100,
-        backgroundColor: t.colors.$neutralAlpha50,
-      })}
-    >
-      {showConfirmation ? (
-        <Col gap={8}>
-          <Heading textVariant='h3'>Cancel {plan.name} Subscription?</Heading>
-          <Text colorScheme='secondary'>
-            You can keep using &ldquo;{plan.name}&rdquo; features until [DATE], after which you will no longer have
-            access.
-          </Text>
-          {hasError && (
-            <Alert colorScheme='danger'>There was a problem canceling your subscription, please try again.</Alert>
-          )}
-          <Flex
-            gap={3}
-            justify='end'
-          >
-            {!isSubmitting && (
-              <Button
-                variant='ghost'
-                size='sm'
-                textVariant='buttonLarge'
-                onClick={() => {
-                  setHasError(false);
-                  setShowConfirmation(false);
-                }}
-              >
-                Keep Subscription
-              </Button>
-            )}
-            <Button
-              variant='solid'
-              colorScheme='danger'
-              size='sm'
-              textVariant='buttonLarge'
-              isLoading={isSubmitting}
-              onClick={cancelSubscription}
-            >
-              Cancel Subscription
-            </Button>
-          </Flex>
-        </Col>
-      ) : (
-        <Button
-          variant='bordered'
-          colorScheme='secondary'
-          size='sm'
-          textVariant='buttonLarge'
-          sx={{
-            width: '100%',
-          }}
-          onClick={() => setShowConfirmation(true)}
-        >
-          Cancel Subscription
-        </Button>
-      )}
-    </Col>
-  );
-};
diff --git a/packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx b/packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx
new file mode 100644
index 00000000000..abf44033e4f
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx
@@ -0,0 +1,160 @@
+import type { __experimental_CommercePlanResource } from '@clerk/types';
+import * as React from 'react';
+
+import { Alert, Box, Button, Col, Flex, Heading, Text } from '../../customizables';
+import { Drawer } from '../../elements';
+import type { PlanPeriod } from './PlanCard';
+import { PlanCardFeaturesList, PlanCardHeader } from './PlanCard';
+
+type DrawerRootProps = React.ComponentProps<typeof Drawer.Root>;
+
+type PlanDetailDrawerProps = {
+  isOpen: DrawerRootProps['open'];
+  setIsOpen: DrawerRootProps['onOpenChange'];
+  portalProps?: DrawerRootProps['portalProps'];
+  strategy: DrawerRootProps['strategy'];
+  plan?: __experimental_CommercePlanResource;
+  planPeriod: PlanPeriod;
+  setPlanPeriod: (p: PlanPeriod) => void;
+};
+
+export function PlanDetailDrawer({
+  isOpen,
+  setIsOpen,
+  portalProps,
+  strategy,
+  plan,
+  planPeriod,
+  setPlanPeriod,
+}: PlanDetailDrawerProps) {
+  if (!plan) {
+    return null;
+  }
+  const hasFeatures = plan.features.length > 0;
+  return (
+    <Drawer.Root
+      open={isOpen}
+      onOpenChange={setIsOpen}
+      strategy={strategy}
+      portalProps={portalProps}
+    >
+      <Drawer.Overlay />
+      <Drawer.Content>
+        <Drawer.Header
+          sx={t =>
+            !hasFeatures
+              ? {
+                  flex: 1,
+                  borderBottomWidth: 0,
+                  background: t.colors.$colorBackground,
+                }
+              : null
+          }
+        >
+          <PlanCardHeader
+            plan={plan}
+            planPeriod={planPeriod}
+            setPlanPeriod={setPlanPeriod}
+            closeSlot={<Drawer.Close />}
+          />
+        </Drawer.Header>
+        {hasFeatures ? (
+          <Drawer.Body>
+            <Box
+              sx={t => ({
+                padding: t.space.$4,
+              })}
+            >
+              <PlanCardFeaturesList plan={plan} />
+            </Box>
+          </Drawer.Body>
+        ) : null}
+        <CancelFooter
+          plan={plan}
+          handleClose={() => setIsOpen(false)}
+        />
+      </Drawer.Content>
+    </Drawer.Root>
+  );
+}
+
+const CancelFooter = ({ plan }: { plan: __experimental_CommercePlanResource; handleClose: () => void }) => {
+  // const { __experimental_commerce } = useClerk();
+  const [showConfirmation, setShowConfirmation] = React.useState(false);
+  const [isSubmitting, setIsSubmitting] = React.useState(false);
+  const [hasError, setHasError] = React.useState(false);
+
+  const cancelSubscription = async () => {
+    setHasError(false);
+    setIsSubmitting(true);
+
+    // TODO: we need to get a handle on the subscription object in order to cancel it,
+    // but this method doesn't exist yet.
+    //
+    // await subscription.cancel().then(() => {
+    //   setIsSubmitting(false);
+    //   handleClose();
+    // }).catch(() => { setHasError(true); setIsSubmitting(false); });
+  };
+
+  // TODO: remove when we can hook up cancel button
+  // return null;
+
+  return (
+    <Drawer.Footer>
+      {showConfirmation ? (
+        <Col gap={8}>
+          <Heading textVariant='h3'>Cancel {plan.name} Subscription?</Heading>
+          <Text colorScheme='secondary'>
+            You can keep using &ldquo;{plan.name}&rdquo; features until [DATE], after which you will no longer have
+            access.
+          </Text>
+          {hasError && (
+            <Alert colorScheme='danger'>There was a problem canceling your subscription, please try again.</Alert>
+          )}
+          <Flex
+            gap={3}
+            justify='end'
+          >
+            {!isSubmitting && (
+              <Button
+                variant='ghost'
+                size='sm'
+                textVariant='buttonLarge'
+                onClick={() => {
+                  setHasError(false);
+                  setShowConfirmation(false);
+                }}
+              >
+                Keep Subscription
+              </Button>
+            )}
+            <Button
+              variant='solid'
+              colorScheme='danger'
+              size='sm'
+              textVariant='buttonLarge'
+              isLoading={isSubmitting}
+              onClick={cancelSubscription}
+            >
+              Cancel Subscription
+            </Button>
+          </Flex>
+        </Col>
+      ) : (
+        <Button
+          variant='bordered'
+          colorScheme='secondary'
+          size='sm'
+          textVariant='buttonLarge'
+          sx={{
+            width: '100%',
+          }}
+          onClick={() => setShowConfirmation(true)}
+        >
+          Cancel Subscription
+        </Button>
+      )}
+    </Drawer.Footer>
+  );
+};
diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx
index 6b788ae4472..8b29e3a5227 100644
--- a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx
+++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx
@@ -2,18 +2,20 @@ import { useClerk } from '@clerk/shared/react';
 import type { __experimental_CommercePlanResource, __experimental_PricingTableProps } from '@clerk/types';
 import { useState } from 'react';
 
+import { PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
 import { __experimental_CheckoutContext, usePricingTableContext } from '../../contexts';
 import { Box, descriptors } from '../../customizables';
 import { useFetch } from '../../hooks';
 import { InternalThemeProvider } from '../../styledSystem';
 import { __experimental_Checkout } from '../Checkout';
+import type { PlanPeriod } from './PlanCard';
 import { PlanCard } from './PlanCard';
-import { PlanDetailBlade } from './PlanDetailBlade';
+import { PlanDetailDrawer } from './PlanDetailDrawer';
 
 export const __experimental_PricingTable = (props: __experimental_PricingTableProps) => {
   const { __experimental_commerce } = useClerk();
   const { mode = 'mounted' } = usePricingTableContext();
-  const [planPeriod, setPlanPeriod] = useState('month');
+  const [planPeriod, setPlanPeriod] = useState<PlanPeriod>('month');
   const [selectedPlan, setSelectedPlan] = useState<__experimental_CommercePlanResource>();
   const [showCheckout, setShowCheckout] = useState(false);
   const [showPlanDetail, setShowPlanDetail] = useState(false);
@@ -57,8 +59,8 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr
           <PlanCard
             key={plan.id}
             plan={plan}
-            period={planPeriod}
-            setPeriod={setPlanPeriod}
+            planPeriod={planPeriod}
+            setPlanPeriod={setPlanPeriod}
             onSelect={selectPlan}
             props={props}
             isCompact={isCompact}
@@ -69,8 +71,8 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr
         value={{
           componentName: 'Checkout',
           mode,
-          isShowingBlade: showCheckout,
-          handleCloseBlade: () => setShowCheckout(false),
+          isOpen: showCheckout,
+          setIsOpen: setShowCheckout,
         }}
       >
         {/*TODO: Used by InvisibleRootBox, can we simplify? */}
@@ -81,10 +83,16 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr
           />
         </div>
       </__experimental_CheckoutContext.Provider>
-      <PlanDetailBlade
+      <PlanDetailDrawer
         isOpen={showPlanDetail}
-        handleClose={() => setShowPlanDetail(false)}
+        setIsOpen={setShowPlanDetail}
         plan={selectedPlan}
+        planPeriod={planPeriod}
+        setPlanPeriod={setPlanPeriod}
+        strategy={mode === 'mounted' ? 'fixed' : 'absolute'}
+        portalProps={{
+          id: mode === 'modal' ? PROFILE_CARD_SCROLLBOX_ID : undefined,
+        }}
       />
     </InternalThemeProvider>
   );
diff --git a/packages/clerk-js/src/ui/constants.ts b/packages/clerk-js/src/ui/constants.ts
index 1752d530dfc..c7dd82fe64e 100644
--- a/packages/clerk-js/src/ui/constants.ts
+++ b/packages/clerk-js/src/ui/constants.ts
@@ -13,3 +13,5 @@ export const USER_BUTTON_ITEM_ID = {
   MANAGE_ACCOUNT: 'manageAccount',
   SIGN_OUT: 'signOut',
 };
+
+export const PROFILE_CARD_SCROLLBOX_ID = 'clerk-profileCardScrollBox';
diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
index 38890433cff..4a4883ab9eb 100644
--- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
+++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
@@ -92,6 +92,14 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
   'dividerText',
   'dividerLine',
 
+  'drawerBackdrop',
+  'drawerContent',
+  'drawerHeader',
+  'drawerTitle',
+  'drawerBody',
+  'drawerFooter',
+  'drawerClose',
+
   'formHeader',
   'formHeaderTitle',
   'formHeaderSubtitle',
@@ -221,6 +229,7 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
   'planCardDescription',
   'planCardAvatarBadgeContainer',
   'planCardAvatar',
+  'planCardBadge',
   'planCardFeatures',
   'planCardFeaturesList',
   'planCardFeaturesListItem',
diff --git a/packages/clerk-js/src/ui/elements/Avatar.tsx b/packages/clerk-js/src/ui/elements/Avatar.tsx
index 0c960467858..d87c691fa2e 100644
--- a/packages/clerk-js/src/ui/elements/Avatar.tsx
+++ b/packages/clerk-js/src/ui/elements/Avatar.tsx
@@ -107,7 +107,7 @@ const InitialsAvatarFallback = (props: { initials: string }) => {
   return (
     <Text
       as='span'
-      sx={{ ...common.centeredFlex('inline-flex'), width: '100%' }}
+      sx={t => ({ ...common.centeredFlex('inline-flex'), width: '100%', color: t.colors.$colorText })}
     >
       {initials}
     </Text>
diff --git a/packages/clerk-js/src/ui/elements/Disclosure.tsx b/packages/clerk-js/src/ui/elements/Disclosure.tsx
index b667ce39f8b..6731e394716 100644
--- a/packages/clerk-js/src/ui/elements/Disclosure.tsx
+++ b/packages/clerk-js/src/ui/elements/Disclosure.tsx
@@ -123,17 +123,17 @@ interface ContentProps {
 
 const Content = React.forwardRef<HTMLDivElement, ContentProps>(({ children }, ref) => {
   const context = React.useContext(DisclosureContext);
+  const prefersReducedMotion = usePrefersReducedMotion();
+  const { animations: layoutAnimations } = useAppearance().parsedLayout;
   if (!context) {
     throw new Error('Disclosure.Content must be used within Disclosure.Root');
   }
   const { isOpen, id } = context;
-  const prefersReducedMotion = usePrefersReducedMotion();
-  const { animations: appearanceAnimations } = useAppearance().parsedLayout;
+  const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
   const animation: ThemableCssProp = t => ({
-    transition:
-      appearanceAnimations && !prefersReducedMotion
-        ? `grid-template-rows ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`
-        : 'none',
+    transition: isMotionSafe
+      ? `grid-template-rows ${t.transitionDuration.$slower} ${t.transitionTiming.$slowBezier}`
+      : 'none',
   });
 
   return (
diff --git a/packages/clerk-js/src/ui/elements/Drawer.tsx b/packages/clerk-js/src/ui/elements/Drawer.tsx
new file mode 100644
index 00000000000..d65ebc68991
--- /dev/null
+++ b/packages/clerk-js/src/ui/elements/Drawer.tsx
@@ -0,0 +1,416 @@
+import { useSafeLayoutEffect } from '@clerk/shared/react/index';
+import type { UseDismissProps, UseFloatingOptions } from '@floating-ui/react';
+import {
+  FloatingFocusManager,
+  FloatingPortal,
+  useClick,
+  useDismiss,
+  useFloating,
+  useInteractions,
+  useMergeRefs,
+  useRole,
+  useTransitionStyles,
+} from '@floating-ui/react';
+import * as React from 'react';
+
+import { transitionDurationValues, transitionTiming } from '../../ui/foundations/transitions';
+import { Box, descriptors, Flex, Heading, Icon, useAppearance } from '../customizables';
+import { usePrefersReducedMotion } from '../hooks';
+import { useScrollLock } from '../hooks/useScrollLock';
+import { Close as CloseIcon } from '../icons';
+import type { ThemableCssProp } from '../styledSystem';
+import { common, InternalThemeProvider } from '../styledSystem';
+import { colors } from '../utils';
+import { IconButton } from './IconButton';
+
+type FloatingPortalProps = React.ComponentProps<typeof FloatingPortal>;
+
+/* -------------------------------------------------------------------------------------------------
+ * Drawer Context
+ * -----------------------------------------------------------------------------------------------*/
+
+interface DrawerContext {
+  isOpen: boolean;
+  setIsOpen: (open: boolean) => void;
+  strategy: UseFloatingOptions['strategy'];
+  refs: ReturnType<typeof useFloating>['refs'];
+  context: ReturnType<typeof useFloating>['context'];
+  getFloatingProps: ReturnType<typeof useInteractions>['getFloatingProps'];
+  portalProps: FloatingPortalProps;
+}
+
+const DrawerContext = React.createContext<DrawerContext | null>(null);
+
+export const useDrawerContext = () => {
+  const context = React.useContext(DrawerContext);
+  if (!context) {
+    throw new Error('Drawer components must be wrapped in <Drawer.Root>');
+  }
+  return context;
+};
+
+/* -------------------------------------------------------------------------------------------------
+ * Drawer.Root
+ * -----------------------------------------------------------------------------------------------*/
+
+interface RootProps {
+  children: React.ReactNode;
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  /**
+   * The strategy to use when positioning the floating element.
+   * @default 'fixed'
+   * @see https://floating-ui.com/docs/useFloating#strategy
+   */
+  strategy?: UseFloatingOptions['strategy'];
+  /**
+   * @see https://floating-ui.com/docs/useFloating
+   */
+  floatingProps?: Omit<UseFloatingOptions, 'open' | 'onOpenChange' | 'strategy' | 'transform'>;
+  /**
+   * @see https://floating-ui.com/docs/FloatingPortal
+   */
+  portalProps?: FloatingPortalProps;
+  /**
+   * @see https://floating-ui.com/docs/useDismiss
+   */
+  dismissProps?: UseDismissProps;
+}
+
+function Root({
+  children,
+  open,
+  onOpenChange,
+  strategy = 'fixed',
+  floatingProps,
+  portalProps,
+  dismissProps,
+}: RootProps) {
+  const { refs, context } = useFloating({
+    open,
+    onOpenChange,
+    transform: false,
+    strategy,
+    ...floatingProps,
+  });
+
+  const { getFloatingProps } = useInteractions([
+    useClick(context),
+    useDismiss(context, dismissProps),
+    useRole(context),
+  ]);
+
+  return (
+    <InternalThemeProvider>
+      <DrawerContext.Provider
+        value={{
+          isOpen: open,
+          setIsOpen: onOpenChange,
+          strategy,
+          portalProps: portalProps || {},
+          refs,
+          context,
+          getFloatingProps,
+        }}
+      >
+        {children}
+      </DrawerContext.Provider>
+    </InternalThemeProvider>
+  );
+}
+
+/* -------------------------------------------------------------------------------------------------
+ * Drawer.Overlay
+ * -----------------------------------------------------------------------------------------------*/
+
+export const FloatingOverlay = React.forwardRef(function FloatingOverlay(
+  props: React.ComponentPropsWithoutRef<typeof Box>,
+  ref: React.ForwardedRef<HTMLDivElement>,
+) {
+  const { disableScrollLock, enableScrollLock } = useScrollLock();
+
+  useSafeLayoutEffect(() => {
+    enableScrollLock();
+
+    return () => {
+      disableScrollLock();
+    };
+  }, []);
+
+  return (
+    <Box
+      ref={ref}
+      {...props}
+      elementDescriptor={descriptors.drawerBackdrop}
+      sx={[
+        t => ({
+          inset: 0,
+          backgroundColor: colors.setAlpha(t.colors.$colorBackground, 0.28),
+        }),
+        props.sx,
+      ]}
+    />
+  );
+});
+
+const Overlay = React.forwardRef<HTMLDivElement>((_, ref) => {
+  const { strategy, context } = useDrawerContext();
+
+  const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
+    initial: { opacity: 0 },
+    open: { opacity: 1 },
+    close: { opacity: 0 },
+    common: {
+      position: strategy,
+      inset: 0,
+      transitionProperty: 'opacity',
+      transitionTimingFunction: transitionTiming.slowBezier,
+    },
+    duration: {
+      open: transitionDurationValues.slower,
+      close: transitionDurationValues.slow,
+    },
+  });
+
+  if (!isMounted) return null;
+
+  return (
+    <FloatingOverlay
+      ref={ref}
+      style={{
+        ...transitionStyles,
+      }}
+    />
+  );
+});
+
+Overlay.displayName = 'Drawer.Overlay';
+
+/* -------------------------------------------------------------------------------------------------
+ * Drawer.Content
+ * -----------------------------------------------------------------------------------------------*/
+
+interface ContentProps {
+  children: React.ReactNode;
+}
+
+const Content = React.forwardRef<HTMLDivElement, ContentProps>(({ children }, ref) => {
+  const prefersReducedMotion = usePrefersReducedMotion();
+  const { animations: layoutAnimations } = useAppearance().parsedLayout;
+  const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
+  const { strategy, portalProps, refs, context, getFloatingProps } = useDrawerContext();
+  const mergedRefs = useMergeRefs([ref, refs.setFloating]);
+
+  const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
+    initial: { transform: 'translateX(100%)' },
+    open: { transform: 'translateX(0)' },
+    close: { transform: 'translateX(100%)' },
+    common: {
+      transitionProperty: 'transform',
+      transitionTimingFunction: transitionTiming.slowBezier,
+    },
+    duration: isMotionSafe
+      ? {
+          open: transitionDurationValues.slower,
+          close: transitionDurationValues.slow,
+        }
+      : 0,
+  });
+
+  if (!isMounted) return null;
+
+  return (
+    <FloatingPortal {...portalProps}>
+      <FloatingFocusManager
+        context={context}
+        modal
+        outsideElementsInert
+        initialFocus={refs.floating}
+      >
+        <Flex
+          ref={mergedRefs}
+          elementDescriptor={descriptors.drawerContent}
+          {...getFloatingProps()}
+          style={transitionStyles}
+          direction='col'
+          sx={t => ({
+            position: strategy,
+            insetBlock: strategy === 'fixed' ? t.space.$3 : 0,
+            insetInlineEnd: strategy === 'fixed' ? t.space.$3 : 0,
+            outline: 0,
+            width: t.sizes.$100,
+            backgroundColor: t.colors.$colorBackground,
+            borderStartStartRadius: t.radii.$xl,
+            borderEndStartRadius: t.radii.$xl,
+            borderEndEndRadius: strategy === 'fixed' ? t.radii.$xl : 0,
+            borderStartEndRadius: strategy === 'fixed' ? t.radii.$xl : 0,
+            borderWidth: t.borderWidths.$normal,
+            borderStyle: t.borderStyles.$solid,
+            borderColor: t.colors.$neutralAlpha100,
+            boxShadow: t.shadows.$cardBoxShadow,
+            overflow: 'hidden',
+            zIndex: t.zIndices.$modal,
+          })}
+        >
+          {children}
+        </Flex>
+      </FloatingFocusManager>
+    </FloatingPortal>
+  );
+});
+
+Overlay.displayName = 'Drawer.Content';
+
+/* -------------------------------------------------------------------------------------------------
+ * Drawer.Header
+ * -----------------------------------------------------------------------------------------------*/
+
+interface HeaderProps {
+  title?: string;
+  children?: React.ReactNode;
+  sx?: ThemableCssProp;
+}
+
+const Header = React.forwardRef<HTMLDivElement, HeaderProps>(({ title, children, sx }, ref) => {
+  return (
+    <Box
+      ref={ref}
+      elementDescriptor={descriptors.drawerHeader}
+      as='header'
+      sx={[
+        t => ({
+          display: 'flex',
+          background: common.mergedColorsBackground(
+            colors.setAlpha(t.colors.$colorBackground, 1),
+            t.colors.$neutralAlpha50,
+          ),
+          borderBlockEndWidth: t.borderWidths.$normal,
+          borderBlockEndStyle: t.borderStyles.$solid,
+          borderBlockEndColor: t.colors.$neutralAlpha100,
+          borderStartStartRadius: t.radii.$xl,
+          borderStartEndRadius: t.radii.$xl,
+          paddingBlock: title ? t.space.$3 : undefined,
+          paddingInline: title ? t.space.$4 : undefined,
+        }),
+        sx,
+      ]}
+    >
+      {title ? (
+        <>
+          <Heading
+            as='h2'
+            elementDescriptor={descriptors.drawerTitle}
+            textVariant='h2'
+            sx={{
+              alignSelf: 'center',
+            }}
+          >
+            {title}
+          </Heading>
+          <Close />
+        </>
+      ) : (
+        children
+      )}
+    </Box>
+  );
+});
+
+/* -------------------------------------------------------------------------------------------------
+ * Drawer.Body
+ * -----------------------------------------------------------------------------------------------*/
+
+interface BodyProps {
+  children: React.ReactNode;
+}
+
+const Body = React.forwardRef<HTMLDivElement, BodyProps>(({ children }, ref) => {
+  return (
+    <Box
+      ref={ref}
+      elementDescriptor={descriptors.drawerBody}
+      sx={{
+        display: 'flex',
+        flexDirection: 'column',
+        flex: 1,
+        overflowY: 'auto',
+        overflowX: 'hidden',
+      }}
+    >
+      {children}
+    </Box>
+  );
+});
+
+/* -------------------------------------------------------------------------------------------------
+ * Drawer.Footer
+ * -----------------------------------------------------------------------------------------------*/
+
+interface FooterProps {
+  children?: React.ReactNode;
+}
+
+const Footer = React.forwardRef<HTMLDivElement, FooterProps>(({ children }, ref) => {
+  return (
+    <Box
+      ref={ref}
+      elementDescriptor={descriptors.drawerFooter}
+      sx={t => ({
+        display: 'flex',
+        background: common.mergedColorsBackground(
+          colors.setAlpha(t.colors.$colorBackground, 1),
+          t.colors.$neutralAlpha50,
+        ),
+        borderBlockStartWidth: t.borderWidths.$normal,
+        borderBlockStartStyle: t.borderStyles.$solid,
+        borderBlockStartColor: t.colors.$neutralAlpha100,
+        borderEndStartRadius: t.radii.$xl,
+        borderEndEndRadius: t.radii.$xl,
+        paddingBlock: t.space.$3,
+        paddingInline: t.space.$4,
+      })}
+    >
+      {children}
+    </Box>
+  );
+});
+
+/* -------------------------------------------------------------------------------------------------
+ * Drawer.Close
+ * -----------------------------------------------------------------------------------------------*/
+
+const Close = React.forwardRef<HTMLButtonElement>((_, ref) => {
+  const { setIsOpen } = useDrawerContext();
+  return (
+    <IconButton
+      ref={ref}
+      elementDescriptor={descriptors.drawerClose}
+      variant='ghost'
+      aria-label='Close drawer'
+      onClick={() => setIsOpen(false)}
+      icon={
+        <Icon
+          icon={CloseIcon}
+          size='sm'
+        />
+      }
+      sx={t => ({
+        color: t.colors.$colorTextSecondary,
+        padding: t.space.$3,
+        marginInlineStart: 'auto',
+      })}
+    />
+  );
+});
+
+Close.displayName = 'Drawer.Close';
+
+export const Drawer = {
+  Root,
+  Overlay,
+  Content,
+  Header,
+  Body,
+  Footer,
+  Close,
+};
diff --git a/packages/clerk-js/src/ui/elements/Modal.tsx b/packages/clerk-js/src/ui/elements/Modal.tsx
index ee4e035aae4..01308703099 100644
--- a/packages/clerk-js/src/ui/elements/Modal.tsx
+++ b/packages/clerk-js/src/ui/elements/Modal.tsx
@@ -54,6 +54,7 @@ export const Modal = withFloatingTree((props: ModalProps) => {
       nodeId={nodeId}
       context={context}
       isOpen={isOpen}
+      outsideElementsInert
     >
       <ModalContext.Provider value={modalCtx}>
         <Flex
diff --git a/packages/clerk-js/src/ui/elements/Popover.tsx b/packages/clerk-js/src/ui/elements/Popover.tsx
index f01544783c0..52f48cd467d 100644
--- a/packages/clerk-js/src/ui/elements/Popover.tsx
+++ b/packages/clerk-js/src/ui/elements/Popover.tsx
@@ -8,12 +8,26 @@ type PopoverProps = PropsWithChildren<{
   nodeId?: string;
   isOpen?: boolean;
   initialFocus?: number | React.MutableRefObject<HTMLElement | null>;
+  /**
+   * Determines whether outside elements are inert when modal is enabled. This enables pointer modality without a backdrop.
+   * @default false
+   */
+  outsideElementsInert?: boolean;
   order?: Array<'reference' | 'floating' | 'content'>;
   portal?: boolean;
 }>;
 
 export const Popover = (props: PopoverProps) => {
-  const { context, initialFocus, order = ['reference', 'content'], nodeId, isOpen, portal = true, children } = props;
+  const {
+    context,
+    initialFocus,
+    outsideElementsInert = false,
+    order = ['reference', 'content'],
+    nodeId,
+    isOpen,
+    portal = true,
+    children,
+  } = props;
 
   if (portal) {
     return (
@@ -23,6 +37,7 @@ export const Popover = (props: PopoverProps) => {
             <FloatingFocusManager
               context={context}
               initialFocus={initialFocus}
+              outsideElementsInert={outsideElementsInert}
               order={order}
             >
               <>{children}</>
diff --git a/packages/clerk-js/src/ui/elements/ProfileCard/ProfileCardContent.tsx b/packages/clerk-js/src/ui/elements/ProfileCard/ProfileCardContent.tsx
index 00eace10d8f..f7c735c9c1e 100644
--- a/packages/clerk-js/src/ui/elements/ProfileCard/ProfileCardContent.tsx
+++ b/packages/clerk-js/src/ui/elements/ProfileCard/ProfileCardContent.tsx
@@ -1,5 +1,6 @@
 import React from 'react';
 
+import { PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
 import { Col, descriptors } from '../../customizables';
 import { useRouter } from '../../router';
 import { common, mqu } from '../../styledSystem';
@@ -42,6 +43,7 @@ export const ProfileCardContent = (props: ProfileCardContentProps) => {
         borderColor: t.colors.$neutralAlpha50,
         boxShadow: t.shadows.$cardContentShadow,
       })}
+      id={PROFILE_CARD_SCROLLBOX_ID}
     >
       <Col
         elementDescriptor={descriptors.pageScrollBox}
diff --git a/packages/clerk-js/src/ui/elements/index.ts b/packages/clerk-js/src/ui/elements/index.ts
index 6810df6281d..6914c722e5a 100644
--- a/packages/clerk-js/src/ui/elements/index.ts
+++ b/packages/clerk-js/src/ui/elements/index.ts
@@ -12,8 +12,9 @@ export * from './ClipboardInput';
 export * from './CodeControl';
 export * from './contexts';
 export * from './DevModeNotice';
-export * from './Divider';
 export * from './Disclosure';
+export * from './Divider';
+export * from './Drawer';
 export * from './ErrorCard';
 export * from './Form';
 export * from './FormattedPhoneNumber';
@@ -46,6 +47,7 @@ export * from './ProfileCard';
 export * from './ReversibleContainer';
 export * from './RootBox';
 export * from './Section';
+export * from './SegmentedControl';
 export * from './Select';
 export * from './SuccessPage';
 export * from './Tabs';
@@ -58,4 +60,3 @@ export * from './UserPreview';
 export * from './VerificationCodeCard';
 export * from './VerificationLinkCard';
 export * from './withAvatarShimmer';
-export * from './SegmentedControl';
diff --git a/packages/clerk-js/src/ui/foundations/defaultFoundations.ts b/packages/clerk-js/src/ui/foundations/defaultFoundations.ts
index 73e7bd4dd2d..b2650a585f4 100644
--- a/packages/clerk-js/src/ui/foundations/defaultFoundations.ts
+++ b/packages/clerk-js/src/ui/foundations/defaultFoundations.ts
@@ -3,7 +3,7 @@ import { colors } from './colors';
 import { opacity } from './opacity';
 import { shadows } from './shadows';
 import { radii, sizes, space } from './sizes';
-import { transitionDuration, transitionProperty, transitionTiming } from './transitions';
+import { transitionDuration, transitionDurationValues, transitionProperty, transitionTiming } from './transitions';
 import { fonts, fontSizes, fontStyles, fontWeights, letterSpacings, lineHeights } from './typography';
 import { zIndices } from './zIndices';
 
@@ -22,6 +22,7 @@ const defaultInternalThemeFoundations = Object.freeze({
   transitionProperty,
   transitionTiming,
   transitionDuration,
+  transitionDurationValues,
   opacity,
   borderStyles,
   borderWidths,
diff --git a/packages/clerk-js/src/ui/foundations/transitions.ts b/packages/clerk-js/src/ui/foundations/transitions.ts
index 5642e9dc57a..55036490418 100644
--- a/packages/clerk-js/src/ui/foundations/transitions.ts
+++ b/packages/clerk-js/src/ui/foundations/transitions.ts
@@ -1,13 +1,22 @@
-const transitionDuration = Object.freeze({
-  slowest: '600ms',
-  slower: '280ms',
-  slow: '200ms',
-  fast: '120ms',
-  focusRing: '200ms',
-  controls: '100ms',
-  textField: '450ms',
+const transitionDurationValues = Object.freeze({
+  slowest: 600,
+  slower: 280,
+  slow: 200,
+  fast: 120,
+  focusRing: 200,
+  controls: 100,
+  textField: 450,
 } as const);
 
+const toMs = (value: number) => `${value}ms`;
+
+const transitionDuration = Object.freeze(
+  Object.fromEntries(Object.entries(transitionDurationValues).map(([key, value]) => [key, toMs(value)])) as Record<
+    keyof typeof transitionDurationValues,
+    string
+  >,
+);
+
 const transitionProperty = Object.freeze({
   common: 'background-color,background,border-color,color,fill,stroke,opacity,box-shadow,transform',
 } as const);
@@ -18,4 +27,4 @@ const transitionTiming = Object.freeze({
   slowBezier: 'cubic-bezier(0.16, 1, 0.3, 1)',
 } as const);
 
-export { transitionDuration, transitionTiming, transitionProperty };
+export { transitionDuration, transitionTiming, transitionProperty, transitionDurationValues };
diff --git a/packages/clerk-js/src/ui/hooks/index.ts b/packages/clerk-js/src/ui/hooks/index.ts
index 1e0b862ae4e..92dc877a1de 100644
--- a/packages/clerk-js/src/ui/hooks/index.ts
+++ b/packages/clerk-js/src/ui/hooks/index.ts
@@ -1,21 +1,22 @@
+export * from './useCheckout';
+export * from './useClerkModalStateParams';
+export * from './useClipboard';
+export * from './useDebounce';
 export * from './useDelayedVisibility';
-export * from './useWindowEventListener';
 export * from './useEmailLink';
-export * from './useClipboard';
 export * from './useEnabledThirdPartyProviders';
+export * from './useEnterpriseSSOLink';
 export * from './useFetch';
 export * from './useInView';
 export * from './useLoadingStatus';
+export * from './useLocalStorage';
+export * from './useNavigateToFlowStart';
 export * from './usePassword';
 export * from './usePasswordComplexity';
 export * from './usePopover';
 export * from './usePrefersReducedMotion';
-export * from './useLocalStorage';
 export * from './useResizeObserver';
 export * from './useSafeState';
+export * from './useScrollLock';
 export * from './useSearchInput';
-export * from './useDebounce';
-export * from './useClerkModalStateParams';
-export * from './useNavigateToFlowStart';
-export * from './useEnterpriseSSOLink';
-export * from './useCheckout';
+export * from './useWindowEventListener';
diff --git a/packages/clerk-js/src/ui/polishedAppearance.ts b/packages/clerk-js/src/ui/polishedAppearance.ts
index 5de90e4f628..4ffda9ecf28 100644
--- a/packages/clerk-js/src/ui/polishedAppearance.ts
+++ b/packages/clerk-js/src/ui/polishedAppearance.ts
@@ -221,6 +221,10 @@ export const polishedAppearance: Appearance = {
         borderWidth: 0,
         boxShadow: `${theme.shadows.$cardBoxShadow}, ${BORDER_SHADOW_LENGTH} ${theme.colors.$neutralAlpha100}`,
       },
+      drawerContent: {
+        borderWidth: 0,
+        boxShadow: `${theme.shadows.$cardBoxShadow}, ${BORDER_SHADOW_LENGTH} ${theme.colors.$neutralAlpha100}`,
+      },
       popoverBox: {
         borderWidth: 0,
         boxShadow: `${theme.shadows.$cardBoxShadow}, ${BORDER_SHADOW_LENGTH} ${theme.colors.$neutralAlpha100}`,
diff --git a/packages/clerk-js/src/ui/primitives/Dd.tsx b/packages/clerk-js/src/ui/primitives/Dd.tsx
index fa6d06e8f4a..15d28c26a05 100644
--- a/packages/clerk-js/src/ui/primitives/Dd.tsx
+++ b/packages/clerk-js/src/ui/primitives/Dd.tsx
@@ -1,11 +1,15 @@
 import React from 'react';
 
-export const Dd = React.forwardRef<
-  HTMLDListElement,
-  React.DetailedHTMLProps<React.HTMLAttributes<HTMLDListElement>, HTMLDListElement>
->((props, ref) => {
+import type { PrimitiveProps } from '../styledSystem';
+import type { BoxProps } from './Box';
+import { Box } from './Box';
+
+export type DdProps = PrimitiveProps<'dd'> & Omit<BoxProps, 'as'>;
+
+export const Dd = React.forwardRef<HTMLTableCellElement, DdProps>((props, ref) => {
   return (
-    <dd
+    <Box
+      as='dd'
       {...props}
       ref={ref}
     />
diff --git a/packages/clerk-js/src/ui/primitives/Dl.tsx b/packages/clerk-js/src/ui/primitives/Dl.tsx
index be90dbf1b79..d1921fba210 100644
--- a/packages/clerk-js/src/ui/primitives/Dl.tsx
+++ b/packages/clerk-js/src/ui/primitives/Dl.tsx
@@ -1,11 +1,15 @@
 import React from 'react';
 
-export const Dl = React.forwardRef<
-  HTMLDListElement,
-  React.DetailedHTMLProps<React.HTMLAttributes<HTMLDListElement>, HTMLDListElement>
->((props, ref) => {
+import type { PrimitiveProps } from '../styledSystem';
+import type { BoxProps } from './Box';
+import { Box } from './Box';
+
+export type DlProps = PrimitiveProps<'dl'> & Omit<BoxProps, 'as'>;
+
+export const Dl = React.forwardRef<HTMLTableCellElement, DlProps>((props, ref) => {
   return (
-    <dl
+    <Box
+      as='dl'
       {...props}
       ref={ref}
     />
diff --git a/packages/clerk-js/src/ui/primitives/Dt.tsx b/packages/clerk-js/src/ui/primitives/Dt.tsx
index ebe0ec18fde..2d55e67ec6b 100644
--- a/packages/clerk-js/src/ui/primitives/Dt.tsx
+++ b/packages/clerk-js/src/ui/primitives/Dt.tsx
@@ -1,11 +1,15 @@
 import React from 'react';
 
-export const Dt = React.forwardRef<
-  HTMLDListElement,
-  React.DetailedHTMLProps<React.HTMLAttributes<HTMLDListElement>, HTMLDListElement>
->((props, ref) => {
+import type { PrimitiveProps } from '../styledSystem';
+import type { BoxProps } from './Box';
+import { Box } from './Box';
+
+export type DtProps = PrimitiveProps<'dt'> & Omit<BoxProps, 'as'>;
+
+export const Dt = React.forwardRef<HTMLTableCellElement, DtProps>((props, ref) => {
   return (
-    <dt
+    <Box
+      as='dt'
       {...props}
       ref={ref}
     />
diff --git a/packages/clerk-js/src/ui/styledSystem/animations.ts b/packages/clerk-js/src/ui/styledSystem/animations.ts
index 1975ca4f3f8..6320314e49f 100644
--- a/packages/clerk-js/src/ui/styledSystem/animations.ts
+++ b/packages/clerk-js/src/ui/styledSystem/animations.ts
@@ -92,7 +92,7 @@ const outAnimation = keyframes`
     transform: translateY(0px);
     max-height: 6rem;
     visibility: visible;
-  }  
+  }
   100% {
     opacity: 0;
     transform: translateY(5px);
@@ -128,18 +128,6 @@ const navbarSlideIn = keyframes`
   100% {opacity: 1; transform: translateX(0);}
 `;
 
-const drawerSlideIn = keyframes`
-  0% { opacity: 0; translate: 100% 0; }
-  10% { opacity: 1; }
-  100% { opacity: 1; translate: 0; }
-`;
-
-const drawerSlideOut = keyframes`
-  0% { opacity: 1; translate: 0; }
-  90% { opacity: 1; }
-  100% { opacity: 0; translate: 100% 0; }
-`;
-
 export const animations = {
   spinning,
   dropdownSlideInScaleAndFade,
@@ -155,6 +143,4 @@ export const animations = {
   inDelayAnimation,
   outAnimation,
   notificationAnimation,
-  drawerSlideIn,
-  drawerSlideOut,
 };
diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts
index 743b154c78b..70ca173b6e5 100644
--- a/packages/clerk-js/src/ui/types.ts
+++ b/packages/clerk-js/src/ui/types.ts
@@ -108,8 +108,8 @@ export type __experimental_PricingTableCtx = __experimental_PricingTableProps &
 export type __experimental_CheckoutCtx = __experimental_CheckoutProps & {
   componentName: 'Checkout';
   mode?: ComponentMode;
-  isShowingBlade?: boolean;
-  handleCloseBlade?: () => void;
+  isOpen?: boolean;
+  setIsOpen?: (open: boolean) => void;
 };
 
 export type AvailableComponentCtx =
diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts
index 7e36c95718c..76315ac4b32 100644
--- a/packages/types/src/appearance.ts
+++ b/packages/types/src/appearance.ts
@@ -210,6 +210,14 @@ export type ElementsConfig = {
   dividerText: WithOptions;
   dividerLine: WithOptions;
 
+  drawerBackdrop: WithOptions;
+  drawerContent: WithOptions;
+  drawerHeader: WithOptions;
+  drawerTitle: WithOptions;
+  drawerBody: WithOptions;
+  drawerFooter: WithOptions;
+  drawerClose: WithOptions;
+
   formHeader: WithOptions<never, ErrorState>;
   formHeaderTitle: WithOptions<never, ErrorState>;
   formHeaderSubtitle: WithOptions<never, ErrorState>;
@@ -347,10 +355,11 @@ export type ElementsConfig = {
   planCardHeader: WithOptions;
   planCardAvatarBadgeContainer: WithOptions;
   planCardAvatar: WithOptions;
+  planCardBadge: WithOptions;
   planCardTitle: WithOptions;
   planCardDescription: WithOptions;
   planCardFeatures: WithOptions;
-  planCardFeaturesList: WithOptions;
+  planCardFeaturesList: WithOptions<string>;
   planCardFeaturesListItem: WithOptions<string>;
   planCardAction: WithOptions;
   planCardPeriodToggle: WithOptions;


Reviews:
user: login: alexcarpenter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T22:04:07Z
user: login: aeliox
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T22:06:52Z
user: login: alexcarpenter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T22:11:46Z
user: login: jacekradko
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T19:22:34Z
user: login: brkalow
body: 👏
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T17:43:42Z

Review comments:
diff_hunk: @@ -23,6 +33,7 @@ export const Popover = (props: PopoverProps) => {
             <FloatingFocusManager
               context={context}
               initialFocus={initialFocus}
+              outsideElementsInert={outsideElementsInert}
path: packages/clerk-js/src/ui/elements/Popover.tsx
user: login: alexcarpenter
body: This was needed to ensure when the Drawer is used within the Modal, that the inert attributes were properly removed when closing the Modal.
created_at: 2025-03-12T21:57:14Z
author_association: MEMBER
diff_hunk: @@ -1,13 +1,22 @@
-const transitionDuration = Object.freeze({
-  slowest: '600ms',
-  slower: '280ms',
-  slow: '200ms',
-  fast: '120ms',
-  focusRing: '200ms',
-  controls: '100ms',
-  textField: '450ms',
+const transitionDurationValues = Object.freeze({
+  slowest: 600,
+  slower: 280,
+  slow: 200,
+  fast: 120,
+  focusRing: 200,
+  controls: 100,
+  textField: 450,
 } as const);
 
+const toMs = (value: number) => `${value}ms`;
+
+const transitionDuration = Object.freeze(
+  Object.fromEntries(Object.entries(transitionDurationValues).map(([key, value]) => [key, toMs(value)])) as Record<
+    keyof typeof transitionDurationValues,
+    string
+  >,
+);
path: packages/clerk-js/src/ui/foundations/transitions.ts
user: login: alexcarpenter
body: This change, is to be able to make use of the transition duration values in JS animations.
created_at: 2025-03-12T21:59:17Z
author_association: MEMBER
diff_hunk: @@ -13,44 +13,35 @@ import {
   SimpleButton,
   Span,
   Text,
-  useAppearance,
 } from '../../customizables';
 import { Avatar, SegmentedControl } from '../../elements';
-import { usePrefersReducedMotion } from '../../hooks';
-import { Check, InformationCircle, Minus, Plus } from '../../icons';
+import { useMotionSafe } from '../../hooks';
+import { Check, InformationCircle, Plus, Minus } from '../../icons';
path: packages/clerk-js/src/ui/components/PricingTable/PlanCard.tsx
user: login: aeliox
body: I think the linter is going to yell at you about the order of these
```suggestion
import { Check, InformationCircle, Minus, Plus } from '../../icons';
```
created_at: 2025-03-12T22:02:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -54,6 +54,7 @@ export const Modal = withFloatingTree((props: ModalProps) => {
       nodeId={nodeId}
       context={context}
       isOpen={isOpen}
+      outsideElementsInert
path: packages/clerk-js/src/ui/elements/Modal.tsx
user: login: alexcarpenter
body: This was needed to work with Drawer usage within the Modal. Without it, inert attributes would not be properly removed after closing the Modal. Currently folks are not interacting with elements outside of the modals so there should be no difference in experience.
created_at: 2025-03-12T22:02:49Z
author_association: MEMBER
diff_hunk: @@ -42,6 +43,7 @@ export const ProfileCardContent = (props: ProfileCardContentProps) => {
         borderColor: t.colors.$neutralAlpha50,
         boxShadow: t.shadows.$cardContentShadow,
       })}
+      id={PROFILE_CARD_SCROLLBOX_ID}
path: packages/clerk-js/src/ui/elements/ProfileCard/ProfileCardContent.tsx
user: login: alexcarpenter
body: Instead of passing around a ref, I added a targetable id where we needed to portal the drawer. Let me know if anyone has a better solution.
created_at: 2025-03-12T22:03:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,154 @@
+import type { __experimental_CommercePlanResource } from '@clerk/types';
+import * as React from 'react';
+
+import { Alert, Button, Col, Flex, Heading, Text } from '../../customizables';
+import { Drawer } from '../../elements';
+import type { PlanPeriod } from './PlanCard';
+import { PlanCardFeaturesList, PlanCardHeader } from './PlanCard';
+
+type DrawerRootProps = React.ComponentProps<typeof Drawer.Root>;
+
+type PlanDetailDrawerProps = {
+  isOpen: DrawerRootProps['open'];
+  setIsOpen: DrawerRootProps['onOpenChange'];
+  portalProps?: DrawerRootProps['portalProps'];
+  strategy: DrawerRootProps['strategy'];
+  plan?: __experimental_CommercePlanResource;
+  planPeriod: PlanPeriod;
+  setPlanPeriod: (p: PlanPeriod) => void;
+};
+
+export function PlanDetailDrawer({
+  isOpen,
+  setIsOpen,
+  portalProps,
+  strategy,
+  plan,
+  planPeriod,
+  setPlanPeriod,
+}: PlanDetailDrawerProps) {
+  if (!plan) {
+    return null;
+  }
+  const hasFeatures = plan.features.length > 0;
+  return (
+    <Drawer.Root
+      open={isOpen}
+      onOpenChange={setIsOpen}
+      strategy={strategy}
+      portalProps={portalProps}
+    >
+      <Drawer.Overlay />
+      <Drawer.Content>
+        <Drawer.Header
+          sx={t =>
+            !hasFeatures
+              ? {
+                  flex: 1,
+                  borderBottomWidth: 0,
+                  background: t.colors.$colorBackground,
+                }
+              : null
+          }
+        >
+          <PlanCardHeader
+            plan={plan}
+            planPeriod={planPeriod}
+            setPlanPeriod={setPlanPeriod}
+            closeSlot={<Drawer.Close />}
+          />
+        </Drawer.Header>
+        {hasFeatures ? (
+          <Drawer.Body>
+            <PlanCardFeaturesList plan={plan} />
path: packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx
user: login: aeliox
body: I think formatting of feature list is a bit off here.

<img width="474" alt="Screenshot 2025-03-12 at 3 03 29 PM" src="https://github.com/user-attachments/assets/54b7a9ef-abeb-438f-87bd-e9de1b65c0d5" />

created_at: 2025-03-12T22:04:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,154 @@
+import type { __experimental_CommercePlanResource } from '@clerk/types';
+import * as React from 'react';
+
+import { Alert, Button, Col, Flex, Heading, Text } from '../../customizables';
+import { Drawer } from '../../elements';
+import type { PlanPeriod } from './PlanCard';
+import { PlanCardFeaturesList, PlanCardHeader } from './PlanCard';
+
+type DrawerRootProps = React.ComponentProps<typeof Drawer.Root>;
+
+type PlanDetailDrawerProps = {
+  isOpen: DrawerRootProps['open'];
+  setIsOpen: DrawerRootProps['onOpenChange'];
+  portalProps?: DrawerRootProps['portalProps'];
+  strategy: DrawerRootProps['strategy'];
+  plan?: __experimental_CommercePlanResource;
+  planPeriod: PlanPeriod;
+  setPlanPeriod: (p: PlanPeriod) => void;
+};
+
+export function PlanDetailDrawer({
+  isOpen,
+  setIsOpen,
+  portalProps,
+  strategy,
+  plan,
+  planPeriod,
+  setPlanPeriod,
+}: PlanDetailDrawerProps) {
+  if (!plan) {
+    return null;
+  }
+  const hasFeatures = plan.features.length > 0;
+  return (
+    <Drawer.Root
+      open={isOpen}
+      onOpenChange={setIsOpen}
+      strategy={strategy}
+      portalProps={portalProps}
+    >
+      <Drawer.Overlay />
+      <Drawer.Content>
+        <Drawer.Header
+          sx={t =>
+            !hasFeatures
+              ? {
+                  flex: 1,
+                  borderBottomWidth: 0,
+                  background: t.colors.$colorBackground,
+                }
+              : null
+          }
+        >
+          <PlanCardHeader
+            plan={plan}
+            planPeriod={planPeriod}
+            setPlanPeriod={setPlanPeriod}
+            closeSlot={<Drawer.Close />}
+          />
+        </Drawer.Header>
+        {hasFeatures ? (
+          <Drawer.Body>
+            <PlanCardFeaturesList plan={plan} />
path: packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx
user: login: alexcarpenter
body: Doh, good catch

![Screenshot 2025-03-12 at 6 11 31 PM](https://github.com/user-attachments/assets/26f77cf7-7a89-4805-998f-20657c207c33)

created_at: 2025-03-12T22:11:46Z
author_association: MEMBER
diff_hunk: @@ -46,6 +47,7 @@ export * from './ProfileCard';
 export * from './ReversibleContainer';
 export * from './RootBox';
 export * from './Section';
+export * from './SegmentedControl';
path: packages/clerk-js/src/ui/elements/index.ts
user: login: jacekradko
body: Looks like this is also being exported on L63
created_at: 2025-03-13T19:22:34Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-12T15:41:47Z
body: ###  🦋  Changeset detected

Latest commit: 00033224b74570615dd223123bc48cf982570cb5

**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/clerk-js         | Patch |
| @clerk/types            | 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/localizations    | Patch |
| @clerk/nextjs           | Patch |
| @clerk/nuxt             | Patch |
| @clerk/react-router     | Patch |
| @clerk/clerk-react      | Patch |
| @clerk/remix            | Patch |
| @clerk/shared           | Patch |
| @clerk/tanstack-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/alexcarpenter/commerce-drawer-4?filename=.changeset/grumpy-guests-confess.md&value=---%0A%22%40clerk%2Fclerk-js%22%3A%20patch%0A%22%40clerk%2Ftypes%22%3A%20patch%0A---%0A%0Afeat(clerk-js%2C%20types)%3A%20%60%3CDrawer%20%2F%3E%60%20component%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-12T15:41:52Z
body: [vc]: #v0qGMJMvVm5tp/9IP/RLefuABLilAzuS0ECaCzfpdxU=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjbGVyay1qcy1zYW5kYm94IiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImNsZXJrLWpzLXNhbmRib3gtZ2l0LWFsZXhjYXJwZW50ZXItY29tbWVyY2UtZHJhd2VyLTQuY2xlcmtzdGFnZS5kZXYifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2NsZXJrLXByb2R1Y3Rpb24vY2xlcmstanMtc2FuZGJveC9BMXEzZ3hlSmZBSFBqY1FTVnhLcFE1aFltSlRNIiwicHJldmlld1VybCI6ImNsZXJrLWpzLXNhbmRib3gtZ2l0LWFsZXhjYXJwZW50ZXItY29tbWVyY2UtZHJhd2VyLTQuY2xlcmtzdGFnZS5kZXYiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJyb290RGlyZWN0b3J5IjoicGFja2FnZXMvY2xlcmstanMifV19
**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/A1q3gxeJfAHPjcQSVxKpQ5hYmJTM)) | [Visit Preview](https://clerk-js-sandbox-git-alexcarpenter-commerce-drawer-4.clerkstage.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/clerk-js-sandbox-git-alexcarpenter-commerce-drawer-4.clerkstage.dev?via=pr-comment-feedback-link) | Mar 14, 2025 5:00pm |


author_association: NONE