Skip to content

feat(expo): add native component theming via Expo config plugin#8243

Open
chriscanin wants to merge 4 commits intomainfrom
chris/native-components-theming
Open

feat(expo): add native component theming via Expo config plugin#8243
chriscanin wants to merge 4 commits intomainfrom
chris/native-components-theming

Conversation

@chriscanin
Copy link
Copy Markdown
Member

@chriscanin chriscanin commented Apr 6, 2026

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

Description

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

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-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 6, 2026

🦋 Changeset detected

Latest commit: 19bc65a

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

This PR includes changesets to release 1 package
Name Type
@clerk/expo Minor

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

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Apr 10, 2026 8:46pm

Request Review

@github-actions github-actions bot added the expo label Apr 6, 2026
…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.
@chriscanin chriscanin marked this pull request as ready for review April 8, 2026 17:18
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

Adds native theming support to the Expo Clerk integration. A new Expo config plugin (withClerkTheme) validates and embeds a JSON theme into iOS Info.plist and writes it to Android assets. The Android runtime (ClerkExpoModule) loads android/app/src/main/assets/clerk_theme.json into Clerk.customTheme. Android UI points (ClerkAuthExpoView, ClerkAuthActivity) now pass Clerk.customTheme into AuthView. iOS ClerkViewFactory loads ClerkTheme from Info.plist, exposes lightTheme/darkTheme, and injects the appropriate theme into view controllers and SwiftUI views. A changeset documents the feature.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding native component theming via Expo config plugin.
Description check ✅ Passed The description clearly relates to the changeset, explaining the theme configuration, JSON schema, and implementation details across iOS and Android.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2c06a5f and 221e30c.

📒 Files selected for processing (5)
  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthActivity.kt
  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkAuthExpoView.kt
  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
  • packages/expo/app.plugin.js
  • packages/expo/ios/ClerkViewFactory.swift

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 8, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@8243

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8243

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8243

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8243

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8243

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@8243

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8243

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8243

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8243

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8243

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8243

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8243

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8243

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8243

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8243

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8243

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8243

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8243

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8243

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8243

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8243

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8243

commit: 19bc65a

@seanperez29
Copy link
Copy Markdown

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

'shadow',
];

const HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh hmmmm I didnt consider this when doing the dark mode / light mode switch. I will look into solutions. Good callout.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b5af733 and 19bc65a.

📒 Files selected for processing (2)
  • packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
  • packages/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

Comment on lines +623 to +648
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`);
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants