Repository: clerk/javascript
Pull request #5346: feat(clerk-js): Pass appearance variables to stripe elements.
Author: login: alexcarpenter
Labels: name: clerk-js
Created: 2025-03-13T17:43:52Z | Merged: 2025-03-21T19:05:40Z

## Description

Pass appearance variables to stripe elements.

| BEFORE | AFTER |
|--------|--------|
| ![Screenshot 2025-03-17 at 4 46 45 PM](https://github.com/user-attachments/assets/0e4d2fc9-2de1-4b67-94c9-ece8341b3d48) | ![Screenshot 2025-03-17 at 4 46 02 PM](https://github.com/user-attachments/assets/a3f2950d-9b66-47ee-8c82-21d54a8fed92) | 

Resolves COM-87

## 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/many-forks-burn.md b/.changeset/many-forks-burn.md
new file mode 100644
index 00000000000..1d909dcd12d
--- /dev/null
+++ b/.changeset/many-forks-burn.md
@@ -0,0 +1,5 @@
+---
+'@clerk/clerk-js': patch
+---
+
+Pass appearance variables to stripe elements.
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index 189f6279cb6..bd2cf2c595e 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -20,7 +20,7 @@
     { "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
     { "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
     { "path": "./dist/pricingTable*.js", "maxSize": "5KB" },
-    { "path": "./dist/checkout*.js", "maxSize": "8.8KB" },
+    { "path": "./dist/checkout*.js", "maxSize": "9KB" },
     { "path": "./dist/up-billing-page*.js", "maxSize": "1KB" }
   ]
 }
diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
index 6edeb1167f1..0ab0815dbfb 100644
--- a/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
+++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
@@ -7,15 +7,15 @@ import type {
   ClerkRuntimeError,
 } from '@clerk/types';
 import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
-import type { Stripe } from '@stripe/stripe-js';
+import type { Appearance as StripeAppearance, Stripe } from '@stripe/stripe-js';
 import { useCallback, useEffect, useMemo, useState } from 'react';
 
-import { Box, Button, Col, descriptors, Flex, Form, Icon, Text } from '../../customizables';
+import { Box, Button, Col, descriptors, Flex, Form, Icon, Text, useAppearance } from '../../customizables';
 import { Alert, Disclosure, Divider, Drawer, LineItems, Select, SelectButton, SelectOptionList } from '../../elements';
 import { useFetch } from '../../hooks';
 import { ArrowUpDown, CreditCard } from '../../icons';
 import { animations } from '../../styledSystem';
-import { handleError } from '../../utils';
+import { handleError, normalizeColorString } from '../../utils';
 
 export const CheckoutForm = ({
   stripe,
@@ -27,6 +27,27 @@ export const CheckoutForm = ({
   onCheckoutComplete: (checkout: __experimental_CommerceCheckoutResource) => void;
 }) => {
   const { plan, planPeriod, totals } = checkout;
+  const { colors, fontWeights, fontSizes, radii, space } = useAppearance().parsedInternalTheme;
+  const elementsAppearance: StripeAppearance = {
+    variables: {
+      colorPrimary: normalizeColorString(colors.$primary500),
+      colorBackground: normalizeColorString(colors.$colorInputBackground),
+      colorText: normalizeColorString(colors.$colorText),
+      colorTextSecondary: normalizeColorString(colors.$colorTextSecondary),
+      colorSuccess: normalizeColorString(colors.$success500),
+      colorDanger: normalizeColorString(colors.$danger500),
+      colorWarning: normalizeColorString(colors.$warning500),
+      fontWeightNormal: fontWeights.$normal.toString(),
+      fontWeightMedium: fontWeights.$medium.toString(),
+      fontWeightBold: fontWeights.$bold.toString(),
+      fontSizeXl: fontSizes.$xl,
+      fontSizeLg: fontSizes.$lg,
+      fontSizeSm: fontSizes.$md,
+      fontSizeXs: fontSizes.$sm,
+      borderRadius: radii.$md,
+      spacingUnit: space.$1,
+    },
+  };
   return (
     <Drawer.Body>
       <Box
@@ -77,7 +98,7 @@ export const CheckoutForm = ({
       {stripe && (
         <Elements
           stripe={stripe}
-          options={{ clientSecret: checkout.externalClientSecret }}
+          options={{ clientSecret: checkout.externalClientSecret, appearance: elementsAppearance }}
         >
           <CheckoutFormElements
             checkout={checkout}
diff --git a/packages/clerk-js/src/ui/utils/__tests__/normalizeColorString.test.ts b/packages/clerk-js/src/ui/utils/__tests__/normalizeColorString.test.ts
new file mode 100644
index 00000000000..40ec028c83e
--- /dev/null
+++ b/packages/clerk-js/src/ui/utils/__tests__/normalizeColorString.test.ts
@@ -0,0 +1,105 @@
+import { normalizeColorString } from '../normalizeColorString';
+
+describe('normalizeColorString', () => {
+  beforeEach(() => {
+    jest.spyOn(console, 'warn').mockImplementation(() => {}) as jest.Mock;
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  // Hex color tests
+  test('should keep 3-char hex colors unchanged', () => {
+    expect(normalizeColorString('#123')).toBe('#123');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  test('should keep 6-char hex colors unchanged', () => {
+    expect(normalizeColorString('#123456')).toBe('#123456');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  test('should remove alpha from 4-char hex colors', () => {
+    expect(normalizeColorString('#123F')).toBe('#123');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  test('should remove alpha from 8-char hex colors', () => {
+    expect(normalizeColorString('#12345678')).toBe('#123456');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  test('should warn for invalid hex formats but return the original', () => {
+    expect(normalizeColorString('#12')).toBe('#12');
+    expect(console.warn).toHaveBeenCalledTimes(1);
+
+    (console.warn as jest.Mock).mockClear();
+    expect(normalizeColorString('#12345')).toBe('#12345');
+    expect(console.warn).toHaveBeenCalledTimes(1);
+  });
+
+  // RGB color tests
+  test('should keep rgb format unchanged but normalize whitespace', () => {
+    expect(normalizeColorString('rgb(255, 0, 0)')).toBe('rgb(255, 0, 0)');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  test('should convert rgba to rgb', () => {
+    expect(normalizeColorString('rgba(255, 0, 0, 0.5)')).toBe('rgb(255, 0, 0)');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  test('should handle rgb with whitespace variations', () => {
+    expect(normalizeColorString('rgb(255,0,0)')).toBe('rgb(255, 0, 0)');
+    expect(normalizeColorString('rgb( 255 , 0 , 0 )')).toBe('rgb(255, 0, 0)');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  // HSL color tests
+  test('should keep hsl format unchanged but normalize whitespace', () => {
+    expect(normalizeColorString('hsl(120, 100%, 50%)')).toBe('hsl(120, 100%, 50%)');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  test('should convert hsla to hsl', () => {
+    expect(normalizeColorString('hsla(120, 100%, 50%, 0.8)')).toBe('hsl(120, 100%, 50%)');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  test('should handle hsl with whitespace variations', () => {
+    expect(normalizeColorString('hsl(120,100%,50%)')).toBe('hsl(120, 100%, 50%)');
+    expect(normalizeColorString('hsl( 120 , 100% , 50% )')).toBe('hsl(120, 100%, 50%)');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+
+  // Warning tests for invalid inputs
+  test('should warn for invalid color formats but return the original', () => {
+    expect(normalizeColorString('')).toBe('');
+    expect(console.warn).toHaveBeenCalledTimes(1);
+
+    (console.warn as jest.Mock).mockClear();
+    expect(normalizeColorString('invalid')).toBe('invalid');
+    expect(console.warn).toHaveBeenCalledTimes(1);
+
+    (console.warn as jest.Mock).mockClear();
+    expect(normalizeColorString('rgb(255,0)')).toBe('rgb(255,0)');
+    expect(console.warn).toHaveBeenCalledTimes(1);
+  });
+
+  test('should warn for non-string inputs but return the original or empty string', () => {
+    expect(normalizeColorString(null as any)).toBe('');
+    expect(console.warn).toHaveBeenCalledTimes(1);
+
+    (console.warn as jest.Mock).mockClear();
+    expect(normalizeColorString(123 as any)).toBe(123 as any);
+    expect(console.warn).toHaveBeenCalledTimes(1);
+  });
+
+  // Edge cases
+  test('should handle trimming whitespace', () => {
+    expect(normalizeColorString('  #123  ')).toBe('#123');
+    expect(normalizeColorString('\n rgb(255, 0, 0) \t')).toBe('rgb(255, 0, 0)');
+    expect(console.warn).not.toHaveBeenCalled();
+  });
+});
diff --git a/packages/clerk-js/src/ui/utils/index.ts b/packages/clerk-js/src/ui/utils/index.ts
index aeb8b01f264..c946647108e 100644
--- a/packages/clerk-js/src/ui/utils/index.ts
+++ b/packages/clerk-js/src/ui/utils/index.ts
@@ -25,3 +25,4 @@ export * from './colorOptionToHslaScale';
 export * from './createCustomMenuItems';
 export * from './usernameUtils';
 export * from './web3CallbackErrorHandler';
+export * from './normalizeColorString';
diff --git a/packages/clerk-js/src/ui/utils/normalizeColorString.ts b/packages/clerk-js/src/ui/utils/normalizeColorString.ts
new file mode 100644
index 00000000000..69602749402
--- /dev/null
+++ b/packages/clerk-js/src/ui/utils/normalizeColorString.ts
@@ -0,0 +1,88 @@
+/**
+ * Normalizes color format strings by removing alpha values if present
+ * Handles conversions between:
+ * - Hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA → #RGB or #RRGGBB
+ * - RGB: rgb(r, g, b), rgba(r, g, b, a) → rgb(r, g, b)
+ * - HSL: hsl(h, s%, l%), hsla(h, s%, l%, a) → hsl(h, s%, l%)
+ *
+ * @param colorString - The color string to normalize
+ * @returns The normalized color string without alpha components, or the original string if invalid
+ */
+export function normalizeColorString(colorString: string): string {
+  if (!colorString || typeof colorString !== 'string') {
+    console.warn('Invalid input: color string must be a non-empty string');
+    return colorString || '';
+  }
+
+  const trimmed = colorString.trim();
+
+  // Handle empty strings
+  if (trimmed === '') {
+    console.warn('Invalid input: color string cannot be empty');
+    return '';
+  }
+
+  // Handle hex colors
+  if (trimmed.startsWith('#')) {
+    // Validate hex format
+    if (!/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(trimmed)) {
+      console.warn(`Invalid hex color format: ${colorString}`);
+      return trimmed;
+    }
+
+    // #RGBA format (4 chars)
+    if (trimmed.length === 5) {
+      return '#' + trimmed.slice(1, 4);
+    }
+    // #RRGGBBAA format (9 chars)
+    if (trimmed.length === 9) {
+      return '#' + trimmed.slice(1, 7);
+    }
+    // Regular hex formats (#RGB, #RRGGBB)
+    return trimmed;
+  }
+
+  // Handle rgb/rgba
+  if (/^rgba?\(/.test(trimmed)) {
+    // Extract and normalize rgb values
+    const rgbMatch = trimmed.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
+    if (rgbMatch) {
+      // Already in rgb format, normalize whitespace
+      return `rgb(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]})`;
+    }
+
+    // Extract and normalize rgba values
+    const rgbaMatch = trimmed.match(/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/);
+    if (rgbaMatch) {
+      // Convert rgba to rgb, normalize whitespace
+      return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`;
+    }
+
+    console.warn(`Invalid RGB/RGBA format: ${colorString}`);
+    return trimmed;
+  }
+
+  // Handle hsl/hsla
+  if (/^hsla?\(/.test(trimmed)) {
+    // Extract and normalize hsl values
+    const hslMatch = trimmed.match(/^hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$/);
+    if (hslMatch) {
+      // Already in hsl format, normalize whitespace
+      return `hsl(${hslMatch[1]}, ${hslMatch[2]}%, ${hslMatch[3]}%)`;
+    }
+
+    // Extract and normalize hsla values
+    const hslaMatch = trimmed.match(/^hsla\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([\d.]+)\s*\)$/);
+    if (hslaMatch) {
+      // Convert hsla to hsl, normalize whitespace
+      return `hsl(${hslaMatch[1]}, ${hslaMatch[2]}%, ${hslaMatch[3]}%)`;
+    }
+
+    console.warn(`Invalid HSL/HSLA format: ${colorString}`);
+    return trimmed;
+  }
+
+  // If we reach here, the input is not a recognized color format
+  console.warn(`Unrecognized color format: ${colorString}`);
+  return trimmed;
+}


Reviews:
user: login: panteliselef
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T18:47:17Z
user: login: alexcarpenter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T19:24:23Z
user: login: iagodahlem
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T09:00:35Z
user: login: panteliselef
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T09:27:56Z
user: login: alexcarpenter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T19:05:36Z

Review comments:
diff_hunk: @@ -7,16 +7,40 @@ import type {
   ClerkRuntimeError,
 } from '@clerk/types';
 import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
-import type { Stripe } from '@stripe/stripe-js';
+import type { Appearance as StripeAppearance, Stripe } from '@stripe/stripe-js';
 import { useCallback, useEffect, useMemo, useState } from 'react';
 
-import { Box, Button, Col, descriptors, Flex, Form, Icon, Text } from '../../customizables';
+import { Box, Button, Col, descriptors, Flex, Form, Icon, Text, useAppearance } from '../../customizables';
 import { Alert, Disclosure, Divider, Drawer, LineItems, Select, SelectButton, SelectOptionList } from '../../elements';
 import { useFetch } from '../../hooks';
 import { ArrowUpDown, CreditCard } from '../../icons';
 import { animations } from '../../styledSystem';
 import { handleError } from '../../utils';
 
+/**
+ * Parses different color format strings and normalizes them
+ * Handles conversions between:
+ * - #000 to #000
+ * - rgb(0, 0, 0) to rgb(0, 0, 0) (unchanged)
+ * - rgba(0, 0, 0, 1) to rgb(0, 0, 0) (alpha removed)
+ * - hsl(0, 0%, 0%) to hsl(0, 0%, 0%) (unchanged)
+ * - hsla(0, 0%, 0%, 1) to hsl(0, 0%, 0%) (alpha removed)
+ *
+ * @param colorString - The color string to parse
+ * @returns The normalized color string without alpha components
+ */
+function parseColorString(colorString: string): string {
+  const trimmed = colorString.trim();
+
+  // Early return for hex colors and non-alpha formats
+  if (trimmed.startsWith('#') || trimmed.startsWith('rgb(') || trimmed.startsWith('hsl(')) {
+    return trimmed;
+  }
+
+  // Convert rgba/hsla to rgb/hsl by removing the alpha component
+  return trimmed.replace(/([rgb|hsl])a\((.*),\s*[\d.]+\)/, '$1($2)');
+}
path: packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
user: login: panteliselef
body: Let's add a unit test for that.
created_at: 2025-03-18T18:47:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,16 +7,40 @@ import type {
   ClerkRuntimeError,
 } from '@clerk/types';
 import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
-import type { Stripe } from '@stripe/stripe-js';
+import type { Appearance as StripeAppearance, Stripe } from '@stripe/stripe-js';
 import { useCallback, useEffect, useMemo, useState } from 'react';
 
-import { Box, Button, Col, descriptors, Flex, Form, Icon, Text } from '../../customizables';
+import { Box, Button, Col, descriptors, Flex, Form, Icon, Text, useAppearance } from '../../customizables';
 import { Alert, Disclosure, Divider, Drawer, LineItems, Select, SelectButton, SelectOptionList } from '../../elements';
 import { useFetch } from '../../hooks';
 import { ArrowUpDown, CreditCard } from '../../icons';
 import { animations } from '../../styledSystem';
 import { handleError } from '../../utils';
 
+/**
+ * Parses different color format strings and normalizes them
+ * Handles conversions between:
+ * - #000 to #000
+ * - rgb(0, 0, 0) to rgb(0, 0, 0) (unchanged)
+ * - rgba(0, 0, 0, 1) to rgb(0, 0, 0) (alpha removed)
+ * - hsl(0, 0%, 0%) to hsl(0, 0%, 0%) (unchanged)
+ * - hsla(0, 0%, 0%, 1) to hsl(0, 0%, 0%) (alpha removed)
+ *
+ * @param colorString - The color string to parse
+ * @returns The normalized color string without alpha components
+ */
+function parseColorString(colorString: string): string {
+  const trimmed = colorString.trim();
+
+  // Early return for hex colors and non-alpha formats
+  if (trimmed.startsWith('#') || trimmed.startsWith('rgb(') || trimmed.startsWith('hsl(')) {
+    return trimmed;
+  }
+
+  // Convert rgba/hsla to rgb/hsl by removing the alpha component
+  return trimmed.replace(/([rgb|hsl])a\((.*),\s*[\d.]+\)/, '$1($2)');
+}
path: packages/clerk-js/src/ui/components/Checkout/CheckoutForm.tsx
user: login: alexcarpenter
body: Extracted and unit tests added. Let me know what you think 🫡 
created_at: 2025-03-18T19:24:23Z
author_association: MEMBER
diff_hunk: @@ -1,27 +1,28 @@
-export * from './fromEntries';
+export * from './colorOptionToHslaScale';
+export * from './colors';
 export * from './containsAllOf';
+export * from './createCustomMenuItems';
+export * from './createCustomPages';
 export * from './createInfiniteAccessProxy';
-export * from './intl';
-export * from './colors';
+export * from './createSlug';
+export * from './errorHandler';
+export * from './ExternalElementMounter';
 export * from './factorSorting';
-export * from './sleep';
-export * from './isMobileDevice';
-export * from './phoneUtils';
 export * from './formatSafeIdentifier';
-export * from './removeUndefinedProps';
-export * from './readObjectPath';
-export * from './useFormControl';
-export * from './errorHandler';
-export * from './range';
-export * from './getValidReactChildren';
-export * from './roleLocalizationKey';
+export * from './fromEntries';
 export * from './getRelativeToNowDateKey';
+export * from './getValidReactChildren';
+export * from './intl';
+export * from './isMobileDevice';
 export * from './mergeRefs';
-export * from './createSlug';
+export * from './normalizeColorString';
 export * from './passwordUtils';
-export * from './createCustomPages';
-export * from './ExternalElementMounter';
-export * from './colorOptionToHslaScale';
-export * from './createCustomMenuItems';
+export * from './phoneUtils';
+export * from './range';
+export * from './readObjectPath';
+export * from './removeUndefinedProps';
+export * from './roleLocalizationKey';
+export * from './sleep';
+export * from './useFormControl';
path: packages/clerk-js/src/ui/utils/index.ts
user: login: iagodahlem
body: 👍 for alphabetically organized imports. 🙌 
created_at: 2025-03-21T08:59:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,88 @@
+/**
+ * Normalizes color format strings by removing alpha values if present
+ * Handles conversions between:
+ * - Hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA → #RGB or #RRGGBB
+ * - RGB: rgb(r, g, b), rgba(r, g, b, a) → rgb(r, g, b)
+ * - HSL: hsl(h, s%, l%), hsla(h, s%, l%, a) → hsl(h, s%, l%)
+ *
+ * @param colorString - The color string to normalize
+ * @returns The normalized color string without alpha components, or the original string if invalid
+ */
+export function normalizeColorString(colorString: string): string {
+  if (!colorString || typeof colorString !== 'string') {
+    console.warn('Invalid input: color string must be a non-empty string');
+    return colorString || '';
+  }
+
+  const trimmed = colorString.trim();
+
+  // Handle empty strings
+  if (trimmed === '') {
+    console.warn('Invalid input: color string cannot be empty');
+    return '';
+  }
+
+  // Handle hex colors
+  if (trimmed.startsWith('#')) {
+    // Validate hex format
path: packages/clerk-js/src/ui/utils/normalizeColorString.ts
user: login: panteliselef
body: What happened to the smaller script we had ? Did it end up being insufficient ?
created_at: 2025-03-21T09:27:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+/**
+ * Normalizes color format strings by removing alpha values if present
+ * Handles conversions between:
+ * - Hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA → #RGB or #RRGGBB
+ * - RGB: rgb(r, g, b), rgba(r, g, b, a) → rgb(r, g, b)
+ * - HSL: hsl(h, s%, l%), hsla(h, s%, l%, a) → hsl(h, s%, l%)
+ *
+ * @param colorString - The color string to normalize
+ * @returns The normalized color string without alpha components, or the original string if invalid
+ */
+export function normalizeColorString(colorString: string): string {
+  if (!colorString || typeof colorString !== 'string') {
+    console.warn('Invalid input: color string must be a non-empty string');
+    return colorString || '';
+  }
+
+  const trimmed = colorString.trim();
+
+  // Handle empty strings
+  if (trimmed === '') {
+    console.warn('Invalid input: color string cannot be empty');
+    return '';
+  }
+
+  // Handle hex colors
+  if (trimmed.startsWith('#')) {
+    // Validate hex format
path: packages/clerk-js/src/ui/utils/normalizeColorString.ts
user: login: alexcarpenter
body: for a more global color function, I thought to make it work across more than just our isolated implementation.
created_at: 2025-03-21T19:05:36Z
author_association: MEMBER

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-13T17:43:56Z
body: ###  🦋  Changeset detected

Latest commit: 07a33221428d7995c0a750cc8c3468e4333cb588

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

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

  | Name                    | Type  |
| ----------------------- | ----- |
| @clerk/clerk-js         | Patch |
| @clerk/chrome-extension | Patch |
| @clerk/clerk-expo       | 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/com-87-figure-out-how-to-pass-appearance-options-into-stripe?filename=.changeset/spotty-fireants-turn.md&value=---%0A%22%40clerk%2Fclerk-js%22%3A%20patch%0A---%0A%0Afeat(clerk-js)%3A%20Pass%20appearance%20variables%20to%20stripe%20elements.%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-13T17:43:56Z
body: [vc]: #2Cq8srebBK1+1dG8tkrG+JRvcOg+ytHksl69yU5AZ1g=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjbGVyay1qcy1zYW5kYm94IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2NsZXJrLXByb2R1Y3Rpb24vY2xlcmstanMtc2FuZGJveC9LSHZlcmN2UVJQZEJteVJoSDFUeFF5aWNFdndFIiwicHJldmlld1VybCI6ImNsZXJrLWpzLXNhbmRib3gtZ2l0LWFsZXhjYXJwZW50ZXItY29tLTg3LWZpZ3VyZS1vdXQtaG93LTYxMDU2Ni5jbGVya3N0YWdlLmRldiIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJjbGVyay1qcy1zYW5kYm94LWdpdC1hbGV4Y2FycGVudGVyLWNvbS04Ny1maWd1cmUtb3V0LWhvdy02MTA1NjYuY2xlcmtzdGFnZS5kZXYifSwicm9vdERpcmVjdG9yeSI6InBhY2thZ2VzL2NsZXJrLWpzIn1dfQ==
**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/KHvercvQRPdBmyRhH1TxQyicEvwE)) | [Visit Preview](https://clerk-js-sandbox-git-alexcarpenter-com-87-figure-out-how-610566.clerkstage.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/clerk-js-sandbox-git-alexcarpenter-com-87-figure-out-how-610566.clerkstage.dev?via=pr-comment-feedback-link) | Mar 21, 2025 5:09pm |


author_association: NONE