Skip to content

Conversation

@jacekradko
Copy link
Member

@jacekradko jacekradko commented Jan 21, 2026

Summary

Fixes: USER-4231

This PR removes the clerkJSVariant: 'headless' option and replaces it with a new prefetchUI prop that controls whether the @clerk/ui script is prefetched.

Changes

  • Removed: clerkJSVariant: 'headless' option from all SDK packages
  • Added: prefetchUI prop (boolean | undefined) to control UI bundle prefetching
    • prefetchUI={false} - Disable prefetching the UI bundle (UI will still load on-demand when needed)
    • prefetchUI omitted or undefined - Prefetch UI normally (default behavior)
  • Added: shouldPrefetchClerkUI helper function
  • Added: Codemod to automatically migrate clerkJSVariant="headless" to prefetchUI={false}
  • Renamed: clerkUiCtorclerkUICtor for casing consistency
  • Deprecated: ClerkJs casing in favor of ClerkJS for consistency
    • loadClerkJsScriptloadClerkJSScript
    • clerkJsScriptUrlclerkJSScriptUrl
    • buildClerkJsScriptAttributesbuildClerkJSScriptAttributes
    • setClerkJsLoadingErrorPackageNamesetClerkJSLoadingErrorPackageName
    • LoadClerkJsScriptOptionsLoadClerkJSScriptOptions
    • Old names are deprecated aliases that will be removed in a future major version

Environment Variables

You can disable UI prefetching via environment variable:

  • Next.js: NEXT_PUBLIC_CLERK_PREFETCH_UI=false
  • Astro: PUBLIC_CLERK_PREFETCH_UI=false
  • React Router / TanStack Start: CLERK_PREFETCH_UI=false

Usage

// Disable UI prefetching (e.g., when using Control Components for custom UI)
<ClerkProvider prefetchUI={false}>
  {children}
</ClerkProvider>

Packages Updated

  • @clerk/clerk-js (major)
  • @clerk/shared (major)
  • @clerk/react
  • @clerk/nextjs
  • @clerk/astro
  • @clerk/nuxt
  • @clerk/vue
  • @clerk/react-router
  • @clerk/tanstack-react-start
  • @clerk/expo
  • @clerk/chrome-extension
  • @clerk/express
  • @clerk/upgrade (codemod added)

Checklist

  • pnpm build passes
  • Updated integration tests
  • Updated integration templates
  • Added codemod for migration
  • Added upgrade documentation

Summary by CodeRabbit

  • Breaking Changes

    • Removed clerkJSVariant and the headless bundle; prop names changed (clerkUiUrl → clerkUIUrl, clerkUiCtor → clerkUICtor).
  • New Features

    • Added prefetchUI prop to control UI prefetching (prefetchUI={false} disables).
    • Added environment toggles to disable UI prefetching (PUBLIC_CLERK_PREFETCH_UI / CLERK_PREFETCH_UI_DISABLED).
  • Tests / Docs

    • Updated tests, codemod, and upgrade docs to reflect the prop and env changes.

✏️ Tip: You can customize this high-level summary in your review settings.

nikosdouvlis and others added 8 commits January 9, 2026 13:15
why:
when using clerkJSVariant='headless', applications only need control components and don't require the full UI bundle. loading the unnecessary @clerk/ui script adds overhead without providing value.

what changed:
- clerk-script.tsx: conditionally render clerk-ui script tag only when clerkJSVariant !== 'headless'
- integration template: read NEXT_PUBLIC_CLERK_JS_VARIANT env var and pass to ClerkProvider

users can now set NEXT_PUBLIC_CLERK_JS_VARIANT='headless' to skip loading the ~100KB @clerk/ui bundle when using only control components.
why:
when using clerkJSVariant='headless', applications only need control components and don't require the full UI bundle. loading the unnecessary @clerk/ui script adds overhead without providing value.

what changed:
- build-clerk-hotload-script: skip generating clerk-ui script tag when clerkJsVariant === 'headless'
- create-clerk-instance: getClerkUiEntryChunk returns undefined for headless variant to skip client-side hot-loading

users can now set clerkJSVariant='headless' to skip loading the ~100KB @clerk/ui bundle when using only control components.
why:
when using clerkJSVariant='headless', applications only need control components and don't require the full UI bundle. loading the unnecessary @clerk/ui script adds overhead without providing value.

what changed:
isomorphicClerk's getClerkUiEntryChunk method now returns undefined when clerkJSVariant === 'headless', skipping the loadClerkUiScript call entirely.

users can now set clerkJSVariant='headless' to skip loading the ~100KB @clerk/ui bundle when using only control components.
why:
when using clerkJSVariant='headless', applications only need control components and don't require the full UI bundle. loading the unnecessary @clerk/ui script adds overhead without providing value.

what changed:
clerkPlugin now checks if clerkJSVariant === 'headless' and skips the loadClerkUiScript call, resolving the clerkUiCtorPromise to undefined instead.

users can now set clerkJSVariant='headless' to skip loading the ~100KB @clerk/ui bundle when using only control components.
why:
verify that the headless variant correctly skips clerk-ui script injection across the full integration stack (env var → prop → script rendering).

what changed:
created headless-variant.test.ts that sets CLERK_JS_VARIANT='headless' and asserts clerk-ui script is absent while clerk-js script is present.
…ition

The headless variant is no longer needed now that UI components have been
moved to @clerk/ui. The browser builds are now identical in size.

Changes:
- Add `react-native` export condition in package.json for Expo/RN
- Rename `clerkHeadless` build to `clerkNative` (no chunk splitting)
- Remove `clerkHeadlessBrowser` build (identical to regular browser)
- Update Expo to import from `@clerk/clerk-js` instead of `/headless`
- Deprecate `clerkJSVariant` option (now ignored)
- Delete headless source files and export directory

BREAKING CHANGE: `@clerk/clerk-js/headless` import path removed.
Expo/React Native users should import from `@clerk/clerk-js` directly.
@changeset-bot
Copy link

changeset-bot bot commented Jan 21, 2026

🦋 Changeset detected

Latest commit: c2824b6

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

This PR includes changesets to release 20 packages
Name Type
@clerk/clerk-js Major
@clerk/shared Major
@clerk/react Minor
@clerk/nextjs Minor
@clerk/astro Minor
@clerk/nuxt Minor
@clerk/vue Minor
@clerk/react-router Minor
@clerk/tanstack-react-start Minor
@clerk/expo Minor
@clerk/chrome-extension Minor
@clerk/agent-toolkit Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/testing Patch
@clerk/ui Patch

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

vercel bot commented Jan 21, 2026

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

Project Deployment Review Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jan 23, 2026 4:02pm

Request Review

….com:clerk/javascript into jrad/remove-headless-variant
clerkJSVariant is still used to control whether @clerk/ui loads.
The option just no longer affects the clerk-js URL since the
separate headless build has been removed.
Replace the clerkJSVariant: 'headless' pattern with a cleaner ui prop API:
- ui: false - Skip loading @clerk/ui (for custom UIs)
- ui: { version?, url? } - Load UI with specific version/URL
- ui: undefined (default) - Load UI normally

Also adds shouldLoadClerkUi() helper function to shared package.

Breaking change: clerkJSVariant is removed in favor of ui prop.
@jacekradko jacekradko changed the title [DO NOT MERGE] feat(clerk-js): remove headless variant feat(clerk-js): remove headless variant, add ui prop Jan 21, 2026
Copy link
Member

@nikosdouvlis nikosdouvlis left a comment

Choose a reason for hiding this comment

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

Looking good! Few minor things:

clerkUIVersion in isomorphicClerk.ts

The old code pulled this.options.ui?.version into clerkUiVersion when calling loadClerkUiScript. Now it just spreads this.options, but there's no clerkUIVersion on IsomorphicClerkOptions, so the version never actually makes it to the script URL builder. Was this dead code before, or do we need to add the field?

clerkUiCtor casing

We renamed clerkUiUrlclerkUIUrl to match clerkJSUrl, but clerkUiCtor still has the old casing. Should probably be clerkUICtor while we're at it

Env var naming

CLERK_PREFETCH_UI_DISABLED=true mapping to prefetchUI: false is a double negation. Can we just use CLERK_PREFETCH_UI=false instead? Same for the framework-prefixed variants (NEXT_PUBLIC_CLERK_PREFETCH_UI, PUBLIC_CLERK_PREFETCH_UI, etc.)

Rename internal functions to use consistent ClerkJS casing:
- loadClerkJSScript (deprecated: loadClerkJsScript)
- clerkJSScriptUrl (deprecated: clerkJsScriptUrl)
- buildClerkJSScriptAttributes (deprecated: buildClerkJsScriptAttributes)
- setClerkJSLoadingErrorPackageName (deprecated: setClerkJsLoadingErrorPackageName)
- LoadClerkJSScriptOptions (deprecated: LoadClerkJsScriptOptions)

Deprecated aliases will be removed in a future major version.
- Fix clerkUIVersion: pass ui.version and ui.url to loadClerkUIScript
- Rename clerkUiCtor to clerkUICtor for casing consistency
- Change env var from PREFETCH_UI_DISABLED=true to PREFETCH_UI=false
  (removes double negation for clearer semantics)
@jacekradko
Copy link
Member Author

@nikosdouvlis Addressed your feedback. Thanks for going through it!

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants