Skip to content

feat(ui): migrate Mosaic Button to StyleX#9208

Open
alexcarpenter wants to merge 5 commits into
mainfrom
migrate-mosaic-to-stylex
Open

feat(ui): migrate Mosaic Button to StyleX#9208
alexcarpenter wants to merge 5 commits into
mainfrom
migrate-mosaic-to-stylex

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Jul 21, 2026

Copy link
Copy Markdown
Member

Mosaic Button: Emotion → StyleX. Compile-time atomic CSS, static sheet, zero runtime.

What

  • tokens.stylex.ts: --cl-* vars (oklch + light-dark())
  • props.ts: public contract — .cl-<slot> class + data-<axis> attrs + class/style merge
  • Button → plain StyleX, foldered: components/button/{button,button.styles,index,button.test}
  • isolated build:mosaic (tsdown + rollup-plugin) → dist-mosaic/styles.css, export @clerk/ui/styles.css
  • all 9 @stylexjs eslint rules on src/mosaic/**
  • swingset live preview wired

Notes

  • Mosaic internal/unreleased → empty changeset, no back-compat constraint
  • org views: sxstyle on new Button

@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a8c6010

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 21, 2026 9:29pm
swingset Ready Ready Preview, Comment Jul 21, 2026 9:29pm

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Jul 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9208

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9208

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9208

@clerk/expo

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

@clerk/expo-google-signin

npm i https://pkg.pr.new/@clerk/expo-google-signin@9208

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: a8c6010

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 018fdbd8-5001-48d6-bf89-cc69baf3c8d3

📥 Commits

Reviewing files that changed from the base of the PR and between 7b64fd0 and a8c6010.

📒 Files selected for processing (2)
  • packages/swingset/src/stories/button.stories.tsx
  • packages/ui/src/mosaic/components/button/button.test.tsx
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/ui/src/mosaic/components/button/button.test.tsx
  • packages/swingset/src/stories/button.stories.tsx

📝 Walkthrough

Walkthrough

The PR migrates Mosaic button styling to StyleX by adding tokens, prop helpers, button styles, component exports, tests, isolated CSS extraction, package exports, lint and test integration, and Swingset webpack/PostCSS configuration. Organization views replace sx styling with inline styles, and the Button story defines variant metadata inline.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: migrating the Mosaic Button to StyleX.
Description check ✅ Passed The description matches the changeset well, covering the StyleX migration, build updates, linting, and preview wiring.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/ui/src/mosaic/styles/index.ts (1)

7-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid growing this index.ts re-export barrel.

Move this curated build entry to a named module (for example, mosaic-stylex.ts) and point tsdown.mosaic.config.mts to it. This avoids a circular-dependency hotspot as migrated components accumulate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/mosaic/styles/index.ts` around lines 7 - 22, Move the curated
exports and token type aliases from the styles index module into a dedicated
named entry module such as mosaic-stylex.ts, then update
tsdown.mosaic.config.mts to use that entry. Keep the existing exports and type
unions unchanged, and leave the index module free from further build-entry
growth.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/swingset/next.config.mjs`:
- Around line 74-93: Use a single StyleX CSS extraction owner for Mosaic across
packages/swingset/next.config.mjs:74-93 and
packages/swingset/postcss.config.mjs:15-38. Update the stylexPlugin
configuration in next.config.mjs and the corresponding PostCSS configuration so
only one path emits CSS, while the other performs JS transforms only; preserve
matching StyleX options and avoid duplicate atom output.

In `@packages/swingset/src/stories/button.stories.tsx`:
- Line 16: Update the Button story’s source path to reference the migrated
exporter at the nested button/button.tsx location, ensuring the Story “View
source” link resolves correctly.

In `@packages/ui/package.json`:
- Around line 75-83: Update the package.json build script to invoke the existing
build:mosaic script as part of the normal build sequence, ensuring
dist-mosaic/styles.css is generated before the package is published.

In `@packages/ui/src/mosaic/components/button/button.test.tsx`:
- Around line 13-21: Extend the “applies default variants when none are passed”
test for the Button component to assert that the rendered button has
type="button". Keep the existing default variant assertions unchanged.

---

Nitpick comments:
In `@packages/ui/src/mosaic/styles/index.ts`:
- Around line 7-22: Move the curated exports and token type aliases from the
styles index module into a dedicated named entry module such as
mosaic-stylex.ts, then update tsdown.mosaic.config.mts to use that entry. Keep
the existing exports and type unions unchanged, and leave the index module free
from further build-entry growth.
🪄 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 Plus

Run ID: 8e28f65c-c34e-42fa-8812-9d706f10ac7e

📥 Commits

Reviewing files that changed from the base of the PR and between 5070797 and d70595f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (29)
  • .changeset/mosaic-stylex-migration.md
  • .gitignore
  • eslint.config.mjs
  • package.json
  • packages/swingset/next.config.mjs
  • packages/swingset/package.json
  • packages/swingset/postcss.config.mjs
  • packages/swingset/src/app/globals.css
  • packages/swingset/src/stories/button.stories.tsx
  • packages/ui/package.json
  • packages/ui/src/mosaic/__tests__/props.test.ts
  • packages/ui/src/mosaic/components/__tests__/button.test.tsx
  • packages/ui/src/mosaic/components/button.tsx
  • packages/ui/src/mosaic/components/button/button.styles.ts
  • packages/ui/src/mosaic/components/button/button.test.tsx
  • packages/ui/src/mosaic/components/button/button.tsx
  • packages/ui/src/mosaic/components/button/index.ts
  • packages/ui/src/mosaic/organization/organization-profile-delete-section.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-domains-section.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-leave-section.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-members-panel.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-profile-section.view.tsx
  • packages/ui/src/mosaic/props.ts
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts
  • packages/ui/styles.css.d.ts
  • packages/ui/tsconfig.mosaic.json
  • packages/ui/tsdown.mosaic.config.mts
  • packages/ui/vitest.config.mts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
💤 Files with no reviewable changes (2)
  • packages/ui/src/mosaic/components/tests/button.test.tsx
  • packages/ui/src/mosaic/components/button.tsx

Comment on lines +74 to +93
// Swingset consumes Mosaic from source, so StyleX (`defineVars`/`create`/`props`) must be
// compiled here — otherwise the calls hit the runtime and throw. The plugin transforms the
// aliased `../ui/src/mosaic` files and appends the collected CSS into `globals.css`. Plain
// rules (`useCSSLayers: false`) so they coexist with swingset's own unlayered Tailwind CSS.
// The unplugin only compiles the StyleX *JS* here (turns `defineVars`/`create`/`props`
// into static atom references so nothing hits the runtime). It keeps SWC intact, so
// `next/font` and the Emotion transform keep working. The actual CSS is emitted by the
// `@stylexjs/postcss-plugin` into `globals.css` (`@stylex`), which is why this runs in
// extraction mode (no `runtimeInjection`) — the two share the same StyleX babel version
// and options, so the atom hashes match. The plugin's dev "no CSS asset" warning is
// expected and harmless: CSS delivery is postcss's job, not the bundler's.
config.plugins.push(
stylexPlugin({
dev: process.env.NODE_ENV !== 'production',
unstable_moduleResolution: { type: 'commonJS', rootDir: resolve(__dirname, '../ui') },
useCSSLayers: false,
lightningcssOptions: { targets: stylexTargets },
}),
);

@coderabbitai coderabbitai Bot Jul 21, 2026

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.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== next.config.mjs ==\n'
sed -n '1,220p' packages/swingset/next.config.mjs

printf '\n== postcss.config.mjs ==\n'
sed -n '1,220p' packages/swingset/postcss.config.mjs

printf '\n== StyleX references ==\n'
rg -n "`@stylexjs`|stylexPlugin|runtimeInjection|useCSSLayers|globals.css|mosaic" packages/swingset packages/ui -g '!**/node_modules/**'

Repository: clerk/javascript

Length of output: 26821


🌐 Web query:

@stylexjs/unplugin webpack does it emit css runtimeInjection extraction mode postcss plugin

💡 Result:

The @stylexjs/unplugin for Webpack is designed primarily to aggregate StyleX styles at build time and append them to a CSS asset emitted by the bundler (e.g., using MiniCssExtractPlugin) [1][2][3]. It does not operate via a "runtime injection" extraction mode in production; rather, it consolidates styles into a static CSS file [2][3]. Key details regarding your query: 1. CSS Extraction: The plugin works by intercepting the build process to aggregate CSS and injecting it into an existing CSS asset produced by Webpack [2][3]. If no CSS asset is produced by your Webpack configuration, the plugin will emit a fallback stylesheet named stylex.css [2][3]. It is highly recommended to use a CSS extraction plugin like MiniCssExtractPlugin to ensure a proper injection target exists [1][4][3]. 2. Runtime Injection: While the underlying babel-plugin has a runtimeInjection option (typically used for development or specific client-side needs), it is not the intended "extraction mode" for production builds [5]. Production builds should rely on static CSS extraction to ensure better performance and deterministic output [3]. 3. PostCSS Plugin: StyleX maintains a separate package, @stylexjs/postcss-plugin, which is used for processing the CSS generated by StyleX [2]. It is not a replacement for the unplugin's core role in compiling and aggregating styles, but it can be part of the pipeline to process the aggregated CSS [2]. In summary, for Webpack, you should configure the @stylexjs/unplugin alongside an extraction plugin like MiniCssExtractPlugin to achieve static CSS output [1][4][3]. Avoid using runtime injection in production to prevent unnecessary overhead [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,220p' packages/ui/tsdown.mosaic.config.mts

Repository: clerk/javascript

Length of output: 2082


🌐 Web query:

@stylexjs/unplugin webpack css extraction asset source stylex docs

💡 Result:

To extract CSS when using StyleX with Webpack, you should use the official @stylexjs/unplugin package [1][2][3]. The older @stylexjs/webpack-plugin is largely deprecated in favor of this universal bundler plugin [4][5]. The process for CSS extraction involves two main requirements: integrating the StyleX plugin and configuring a CSS extraction plugin (like MiniCssExtractPlugin) to provide an injection target [1][6][4]. 1. Configure the Plugin: Use stylex.webpack from @stylexjs/unplugin in your webpack.config.js [1][2]. It is recommended to enable useCSSLayers: true for better CSS specificity management [6][7]. 2. Provide an Extraction Target: StyleX aggregates the CSS generated by your application and automatically appends it to an existing CSS asset produced by your build [2][4][3]. You must include a CSS extraction plugin (e.g., MiniCssExtractPlugin) and ensure at least one CSS file is imported in your application code (typically a global stylesheet or entry-point CSS file) so that Webpack creates the necessary CSS asset [1][6][4]. If no CSS assets are emitted by your bundle, @stylexjs/unplugin will default to creating a standalone stylex.css file as a fallback [2][4][3]. Example configuration snippet: const stylex = require('@stylexjs/unplugin').default; const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { module: { rules: [ { test: /.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] }, ], }, plugins: [ stylex.webpack({ useCSSLayers: true }), new MiniCssExtractPlugin, ], }; You can use the cssInjectionTarget option (a function receiving the filename) within the stylex.webpack configuration if you need to specifically select which emitted CSS asset should receive the StyleX styles [4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,120p' packages/swingset/src/app/globals.css

Repository: clerk/javascript

Length of output: 4357


Use one StyleX CSS extraction path for Mosaic. packages/swingset/next.config.mjs and packages/swingset/postcss.config.mjs both process packages/ui/src/mosaic, and the webpack unplugin also emits CSS into the bundle. Keep one owner for CSS output and leave the other path to JS transforms only, or the same atoms can be emitted twice.

📍 Affects 2 files
  • packages/swingset/next.config.mjs#L74-L93 (this comment)
  • packages/swingset/postcss.config.mjs#L15-L38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swingset/next.config.mjs` around lines 74 - 93, Use a single StyleX
CSS extraction owner for Mosaic across packages/swingset/next.config.mjs:74-93
and packages/swingset/postcss.config.mjs:15-38. Update the stylexPlugin
configuration in next.config.mjs and the corresponding PostCSS configuration so
only one path emits CSS, while the other performs JS transforms only; preserve
matching StyleX options and avoid duplicate atom output.

Source: MCP tools

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.

Intentional split: unplugin = JS transform, postcss-plugin = CSS. Checked unplugin webpack.js — it appends CSS only when a .css asset exists, else skips. Swingset runs next dev → no .css asset → unplugin no-ops, postcss sole CSS owner, no dup atoms. (prod build appends once more, but dev-only private tool + atomic CSS idempotent.) Leaving as-is.

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.

@alexcarpenter, understood. The split is intentional: PostCSS is the CSS owner for Swingset development, while the webpack unplugin performs the StyleX JS transform. The production duplicate remains a known, accepted trade-off for this private dev tool, so I won’t press for a change here.


✏️ Learnings added
Learnt from: alexcarpenter
Repo: clerk/javascript PR: 9208
File: packages/swingset/next.config.mjs:74-93
Timestamp: 2026-07-21T21:29:23.020Z
Learning: In `packages/swingset`, the StyleX webpack unplugin intentionally performs the JavaScript transform for source-aliased Mosaic files, while `packages/swingset/postcss.config.mjs` owns CSS emission during `next dev`. Production may append equivalent atomic CSS once more; this is an accepted trade-off because Swingset is a private development tool.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread packages/swingset/src/stories/button.stories.tsx Outdated
Comment thread packages/ui/package.json
Comment thread packages/ui/src/mosaic/components/button/button.test.tsx
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-21T21:29:56.668Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 2
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 2

@clerk/astro

Current version: 3.4.20
Recommended bump: MINOR → 3.5.0

Subpath ./server

🟢 Additions (1)

Added: createRouteMatcher
+ createRouteMatcher: (routes: RouteMatcherParam) => (req: Request) => boolean

Added function createRouteMatcher


@clerk/ui

Current version: 1.25.6
Recommended bump: MINOR → 1.26.0

Subpath ./styles.css

🟢 Additions (1)

Added: ./styles.css

New subpath export ./styles.css


Report generated by Break Check

Last ran on a8c6010.

@alexcarpenter

Copy link
Copy Markdown
Member Author

Nitpick — styles/index.ts barrel → named module: leaving as-is. It's the isolated build entry (tsdown.mosaic.config.mts), imported by nothing else; depends on components/tokens/props one-directionally, so no circular-dep risk.

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.

1 participant