feat(expo): add native component theming via Expo config plugin#8243
feat(expo): add native component theming via Expo config plugin#8243chriscanin wants to merge 4 commits intomainfrom
Conversation
Adds support for customizing native Clerk UI components (sign-in, sign-up,
user profile) on both iOS and Android via a JSON theme configuration file
referenced in the Expo plugin config:
["@clerk/expo", { "theme": "./clerk-theme.json" }]
The JSON schema supports:
- colors: 15 semantic color tokens (primary, background, danger, etc.)
- darkColors: dark mode color overrides (iOS uses @Environment colorScheme,
Android uses ClerkTheme.darkColors)
- fonts: fontFamily string or per-style overrides (iOS only)
- design: borderRadius
Plugin changes:
- Reads and validates the JSON at prebuild time
- iOS: Embeds theme in Info.plist; removes UIUserInterfaceStyle when
darkColors is present to enable system dark mode
- Android: Copies JSON to app assets directory
Native changes:
- iOS: Parses theme from Info.plist, builds light/dark ClerkTheme objects,
applies via .environment(\.clerkTheme) with colorScheme switching
- Android: Parses theme from assets JSON, sets Clerk.customTheme
- Both: AuthView now uses Clerk.customTheme instead of null
🦋 Changeset detectedLatest commit: 19bc65a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…erkDesign signature Two fixes needed to make the Android theme actually take effect: 1. Call loadThemeFromAssets() AFTER Clerk.initialize() instead of before. Clerk.initialize() accepts a `theme` parameter that defaults to null and assigns it to Clerk.customTheme on every call, which was wiping out the theme we just loaded. 2. Use the real ClerkDesign(borderRadius: Dp) constructor signature. The previous code passed nonexistent fontFamily and nullable borderRadius parameters that don't compile against clerk-android-ui.
📝 WalkthroughWalkthroughAdds native theming support to the Expo Clerk integration. A new Expo config plugin ( Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/expo/app.plugin.js`:
- Line 621: HEX_COLOR_REGEX currently allows 3-character hex colors but iOS
colorFromHex only supports 6- and 8-character forms, causing valid-looking
inputs to return nil; update HEX_COLOR_REGEX to remove the 3-char alternative so
it only matches 6- or 8-digit hex (e.g., change
/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/ to a pattern that omits the
{3} branch), and run/update any tests or callers that assume 3-char acceptance;
alternatively, if you prefer to keep 3-char support, implement expansion logic
in the native colorFromHex parser to expand 3-char shorthand to 6-char before
parsing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 4526911d-c482-4634-a181-3952e94714b2
📒 Files selected for processing (5)
packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthActivity.ktpackages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthExpoView.ktpackages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.ktpackages/expo/app.plugin.jspackages/expo/ios/ClerkViewFactory.swift
@clerk/agent-toolkit
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/dev-cli
@clerk/expo
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b5af733345
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
packages/expo/app.plugin.js
Outdated
| 'shadow', | ||
| ]; | ||
|
|
||
| const HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; |
There was a problem hiding this comment.
Disallow 3-digit hex colors in theme validation
The plugin accepts #RGB values via HEX_COLOR_REGEX, but both native parsers only handle 6- or 8-digit hex (parseHexColor on Android and colorFromHex on iOS branch only on lengths 6/8). That means a theme like {"colors":{"primary":"#fff"}} passes prebuild validation and is then silently ignored at runtime, so users think their theme is applied when it is not.
Useful? React with 👍 / 👎.
| return try { | ||
| when (cleaned.length) { | ||
| 6 -> Color(android.graphics.Color.parseColor("#FF$cleaned")) | ||
| 8 -> Color(android.graphics.Color.parseColor("#$cleaned")) |
There was a problem hiding this comment.
Normalize 8-digit hex ordering before Android parseColor
The Android parser forwards 8-digit values directly to android.graphics.Color.parseColor, which interprets them as #AARRGGBB, while iOS parses 8-digit values as RRGGBBAA in colorFromHex. As a result, the same darkColors/colors token can render with different RGB/alpha on each platform, so cross-platform theming is incorrect whenever 8-digit hex values are used.
Useful? React with 👍 / 👎.
…igit hex ordering - Remove 3-digit hex (#RGB) from the prebuild validation regex. Both native parsers only handle 6- or 8-digit hex, so 3-digit values would pass validation but be silently ignored at runtime. - Convert RRGGBBAA to AARRGGBB on Android before passing to parseColor. iOS already parses 8-digit hex as RRGGBBAA, so without this conversion the same color token could render differently on each platform.
| if (themeJson.darkColors) { | ||
| delete modConfig.modResults.UIUserInterfaceStyle; | ||
| console.log('✅ Removed UIUserInterfaceStyle to enable system dark mode'); | ||
| } |
There was a problem hiding this comment.
Something codex's local review spotted on this:
darkColors changes the whole app's appearance policy
When darkColors is present, this deletes UIUserInterfaceStyle from Info.plist. That is an app-wide setting, not a Clerk-only one, so any app intentionally pinned to Light or Dark will silently switch back to system-controlled appearance just to theme Clerk screens.
There was a problem hiding this comment.
Oh hmmmm I didnt consider this when doing the dark mode / light mode switch. I will look into solutions. Good callout.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/expo/app.plugin.js`:
- Around line 623-648: validateThemeJson currently allows non-object shapes for
theme.colors, theme.darkColors, and theme.design because validateColors returns
early for non-objects; change the validation to be strict: in validateColors
throw an Error when colors is present but not an object (use typeof === 'object'
&& colors !== null && !Array.isArray(colors)), and for theme.design require it
to be an object when present (throw if design is not an object or is an
array/null); keep existing checks for VALID_COLOR_KEYS and HEX_COLOR_REGEX and
the design.fontFamily/design.borderRadius type checks, but ensure the initial
presence checks for theme.colors, theme.darkColors, and theme.design validate
object shape and error out instead of silently returning.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro
Run ID: 493cfb37-0ed2-4699-940b-50852d4c2e76
📒 Files selected for processing (2)
packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.ktpackages/expo/app.plugin.js
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
| function validateThemeJson(theme) { | ||
| const validateColors = (colors, label) => { | ||
| if (!colors || typeof colors !== 'object') return; | ||
| for (const [key, value] of Object.entries(colors)) { | ||
| if (!VALID_COLOR_KEYS.includes(key)) { | ||
| console.warn(`⚠️ Clerk theme: unknown color key "${key}" in ${label}, ignoring`); | ||
| continue; | ||
| } | ||
| if (typeof value !== 'string' || !HEX_COLOR_REGEX.test(value)) { | ||
| throw new Error(`Clerk theme: invalid hex color for ${label}.${key}: "${value}"`); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| if (theme.colors) validateColors(theme.colors, 'colors'); | ||
| if (theme.darkColors) validateColors(theme.darkColors, 'darkColors'); | ||
|
|
||
| if (theme.design) { | ||
| if (theme.design.fontFamily != null && typeof theme.design.fontFamily !== 'string') { | ||
| throw new Error(`Clerk theme: design.fontFamily must be a string`); | ||
| } | ||
| if (theme.design.borderRadius != null && typeof theme.design.borderRadius !== 'number') { | ||
| throw new Error(`Clerk theme: design.borderRadius must be a number`); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Schema validator accepts invalid object shapes (merge-blocking)
validateThemeJson does not enforce object types for colors, darkColors, and design (e.g., Line 625 returns early for non-objects). That allows malformed config to pass prebuild validation and then fail or no-op at native runtime.
Proposed fix
function validateThemeJson(theme) {
+ if (!theme || typeof theme !== 'object' || Array.isArray(theme)) {
+ throw new Error(`Clerk theme: root theme must be an object`);
+ }
+
const validateColors = (colors, label) => {
- if (!colors || typeof colors !== 'object') return;
+ if (colors == null) return;
+ if (typeof colors !== 'object' || Array.isArray(colors)) {
+ throw new Error(`Clerk theme: ${label} must be an object`);
+ }
for (const [key, value] of Object.entries(colors)) {
if (!VALID_COLOR_KEYS.includes(key)) {
console.warn(`⚠️ Clerk theme: unknown color key "${key}" in ${label}, ignoring`);
continue;
}
if (typeof value !== 'string' || !HEX_COLOR_REGEX.test(value)) {
throw new Error(`Clerk theme: invalid hex color for ${label}.${key}: "${value}"`);
}
}
};
if (theme.colors) validateColors(theme.colors, 'colors');
if (theme.darkColors) validateColors(theme.darkColors, 'darkColors');
- if (theme.design) {
+ if (theme.design != null) {
+ if (typeof theme.design !== 'object' || Array.isArray(theme.design)) {
+ throw new Error(`Clerk theme: design must be an object`);
+ }
if (theme.design.fontFamily != null && typeof theme.design.fontFamily !== 'string') {
throw new Error(`Clerk theme: design.fontFamily must be a string`);
}
if (theme.design.borderRadius != null && typeof theme.design.borderRadius !== 'number') {
throw new Error(`Clerk theme: design.borderRadius must be a number`);
}
}
}As per coding guidelines, “Only comment on issues that would block merging, ignore minor or stylistic concerns.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/expo/app.plugin.js` around lines 623 - 648, validateThemeJson
currently allows non-object shapes for theme.colors, theme.darkColors, and
theme.design because validateColors returns early for non-objects; change the
validation to be strict: in validateColors throw an Error when colors is present
but not an object (use typeof === 'object' && colors !== null &&
!Array.isArray(colors)), and for theme.design require it to be an object when
present (throw if design is not an object or is an array/null); keep existing
checks for VALID_COLOR_KEYS and HEX_COLOR_REGEX and the
design.fontFamily/design.borderRadius type checks, but ensure the initial
presence checks for theme.colors, theme.darkColors, and theme.design validate
object shape and error out instead of silently returning.
Adds support for customizing native Clerk UI components (sign-in, sign-up, user profile) on both iOS and Android via a JSON theme configuration file referenced in the Expo plugin config:
["@clerk/expo", { "theme": "./clerk-theme.json" }]
The JSON schema supports:
Plugin changes:
Native changes:
Description
Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change