Skip to content

Commit aee0196

Browse files
committed
feat(nextjs): export Mosaic UserButton from an experimental subpath
Adds `@clerk/nextjs/experimental/mosaic` (via `@clerk/react` and `@clerk/ui`) so the Mosaic UserButton can mount directly in a host app's tree, plus the matching `experimental/mosaic/styles.css`. The `@clerk/ui` mosaic entry is now a client boundary in whole (the bundle collapses the per-file directives) and is guarded against Emotion creeping back in, which meant replacing the UserButton trigger skeleton with StyleX.
1 parent 5a9f8e8 commit aee0196

16 files changed

Lines changed: 155 additions & 22 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@clerk/nextjs': minor
3+
'@clerk/react': minor
4+
'@clerk/ui': minor
5+
---
6+
7+
Add an experimental subpath for Mosaic components that mount directly in your app's tree rather than being rendered by clerk-js. `UserButton` is the first one. It reads Clerk through hooks, so a `ClerkProvider` above it is all it needs:
8+
9+
```tsx
10+
import { UserButton } from '@clerk/nextjs/experimental/mosaic';
11+
```
12+
13+
Pair it with the stylesheet, which carries the design tokens and every component rule:
14+
15+
```css
16+
@import '@clerk/nextjs/experimental/mosaic/styles.css' layer(clerk);
17+
```
18+
19+
The surface and the components behind it will change without a major version while they are experimental.
20+
21+
In `@clerk/ui`, the Mosaic stylesheet moves from `@clerk/ui/styles.css` to `@clerk/ui/experimental/mosaic/styles.css` to sit alongside the components it styles. Update the import if you were using it.

.claude/skills/mosaic/references/stylex.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,8 +621,8 @@ export interface PopoverPopupProps extends MosaicComponentProps<'div'> { … }
621621

622622
- **Published** (`build:mosaic``@stylexjs/rollup-plugin`): compiles the
623623
`styles/index.ts` barrel into `dist-mosaic/styles.css`, exported as
624-
`@clerk/ui/styles.css`. Consumers choose the cascade layer at import:
625-
`@import '@clerk/ui/styles.css' layer(components)`.
624+
`@clerk/ui/experimental/mosaic/styles.css`. Consumers choose the cascade layer at import:
625+
`@import '@clerk/ui/experimental/mosaic/styles.css' layer(components)`.
626626
- **Swingset** (source-consumed): `@stylexjs/unplugin/webpack` in `next.config`
627627
transforms StyleX **JS only** (calls → static atoms; SWC/Emotion untouched);
628628
`@stylexjs/postcss-plugin` extracts the **CSS** by replacing `@stylex;` in

packages/nextjs/package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@
5555
"import": "./dist/esm/experimental.js",
5656
"require": "./dist/cjs/experimental.js"
5757
},
58+
"./experimental/mosaic": {
59+
"types": "./dist/types/experimental/mosaic.d.ts",
60+
"import": "./dist/esm/experimental/mosaic.js",
61+
"require": "./dist/cjs/experimental/mosaic.js"
62+
},
63+
"./experimental/mosaic/styles.css": "./dist/experimental/mosaic/styles.css",
5864
"./legacy": {
5965
"types": "./dist/types/legacy.d.ts",
6066
"import": "./dist/esm/legacy.js",
@@ -70,15 +76,15 @@
7076
"webhooks"
7177
],
7278
"scripts": {
73-
"build": "pnpm clean && tsup",
79+
"build": "pnpm clean && tsup && node ../../scripts/copy-mosaic-styles.mjs dist/experimental/mosaic/styles.css",
7480
"build:declarations": "tsc -p tsconfig.declarations.json",
7581
"clean": "rimraf ./dist",
7682
"dev": "tsup --watch",
7783
"dev:pub": "pnpm dev -- --env.publish",
7884
"format": "node ../../scripts/format-package.mjs",
7985
"format:check": "node ../../scripts/format-package.mjs --check",
8086
"lint": "eslint src",
81-
"lint:attw": "attw --pack . --profile node16 --ignore-rules unexpected-module-syntax",
87+
"lint:attw": "attw --pack . --exclude-entrypoints experimental/mosaic/styles.css --profile node16 --ignore-rules unexpected-module-syntax",
8288
"lint:publint": "publint",
8389
"test": "vitest run",
8490
"test:watch": "vitest watch"
@@ -91,6 +97,7 @@
9197
"tslib": "catalog:repo"
9298
},
9399
"devDependencies": {
100+
"@clerk/ui": "workspace:*",
94101
"crypto-es": "^2.1.0",
95102
"next": "15.5.19"
96103
},
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use client';
2+
3+
/**
4+
* Mosaic components mounted directly in the host app's tree, rather than through clerk-js. They
5+
* read Clerk via hooks, so a `ClerkProvider` above them is all they need.
6+
*
7+
* Pair with the stylesheet, which carries the design tokens and every component rule:
8+
*
9+
* ```css
10+
* @import '@clerk/nextjs/experimental/mosaic/styles.css' layer(clerk);
11+
* ```
12+
*
13+
* @experimental The surface and the components behind it are subject to change.
14+
*/
15+
export { UserButton } from '@clerk/react/experimental/mosaic';
16+
export type { UserButtonProps } from '@clerk/react/experimental/mosaic';

packages/react/package.json

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,17 @@
6363
"default": "./dist/experimental.cjs"
6464
}
6565
},
66+
"./experimental/mosaic": {
67+
"import": {
68+
"types": "./dist/experimental/mosaic.d.mts",
69+
"default": "./dist/experimental/mosaic.mjs"
70+
},
71+
"require": {
72+
"types": "./dist/experimental/mosaic.d.cts",
73+
"default": "./dist/experimental/mosaic.cjs"
74+
}
75+
},
76+
"./experimental/mosaic/styles.css": "./dist/experimental/mosaic/styles.css",
6677
"./legacy": {
6778
"import": {
6879
"types": "./dist/legacy.d.mts",
@@ -88,25 +99,25 @@
8899
"dist"
89100
],
90101
"scripts": {
91-
"build": "tsdown",
102+
"build": "tsdown && node ../../scripts/copy-mosaic-styles.mjs dist/experimental/mosaic/styles.css",
92103
"clean": "rimraf ./dist",
93104
"dev": "tsdown --watch",
94105
"dev:pub": "pnpm dev --env.publish",
95106
"format": "node ../../scripts/format-package.mjs",
96107
"format:check": "node ../../scripts/format-package.mjs --check",
97108
"lint": "eslint src",
98-
"lint:attw": "attw --pack . --profile node16",
109+
"lint:attw": "attw --pack . --exclude-entrypoints experimental/mosaic/styles.css --profile node16",
99110
"lint:publint": "publint",
100111
"test": "vitest run",
101112
"test:watch": "vitest watch"
102113
},
103114
"dependencies": {
104115
"@clerk/shared": "workspace:^",
116+
"@clerk/ui": "workspace:^",
105117
"tslib": "catalog:repo"
106118
},
107119
"devDependencies": {
108120
"@clerk/localizations": "workspace:*",
109-
"@clerk/ui": "workspace:*",
110121
"@types/semver": "^7.7.1",
111122
"semver": "^7.8.5",
112123
"yaml": "^2.9.0"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Mosaic components mounted directly in the host app's tree, rather than through clerk-js. They
3+
* read Clerk via `@clerk/shared/react` hooks, so a `ClerkProvider` above them is all they need.
4+
*
5+
* Pair with the stylesheet, which carries the design tokens and every component rule:
6+
*
7+
* ```css
8+
* @import '@clerk/react/experimental/mosaic/styles.css' layer(clerk);
9+
* ```
10+
*
11+
* @experimental The surface and the components behind it are subject to change.
12+
*/
13+
export { UserButton } from '@clerk/ui/experimental/mosaic';
14+
export type { UserButtonProps } from '@clerk/ui/experimental/mosaic';

packages/react/tsdown.config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export default defineConfig((overrideOptions: Options) => {
6363
internal: 'src/internal.ts',
6464
errors: 'src/errors.ts',
6565
experimental: 'src/experimental.ts',
66+
'experimental/mosaic': 'src/experimental/mosaic.ts',
6667
legacy: 'src/legacy.ts',
6768
types: 'src/types/index.ts',
6869
},

packages/swingset/src/stories/menu.component.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,10 @@ const [open, setOpen] = useState(false);
120120
Unlike the slot-recipe components, the Mosaic menu is themed with **StyleX**. Each styled part
121121
carries a stable `.cl-<slot>` class (the slots above) alongside the StyleX atoms. Consumers never
122122
target the hashed atomic classes — override by targeting the `.cl-*` slot from a CSS layer that wins
123-
over `@clerk/ui/styles.css`:
123+
over `@clerk/ui/experimental/mosaic/styles.css`:
124124

125125
```css
126-
@import '@clerk/ui/styles.css' layer(components);
126+
@import '@clerk/ui/experimental/mosaic/styles.css' layer(components);
127127

128128
@layer overrides {
129129
.cl-menu-popup {

packages/swingset/src/stories/popover.component.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,10 @@ them through your own typography (`Heading`, `Text`) inside the surface.
189189
Unlike the slot-recipe components, the Mosaic popover is themed with **StyleX**. Each styled part
190190
carries a stable `.cl-<slot>` class (the slots in the table above) alongside the StyleX atoms.
191191
Consumers never target the hashed atomic classes — override by targeting the `.cl-*` slot from a
192-
CSS layer that wins over `@clerk/ui/styles.css`:
192+
CSS layer that wins over `@clerk/ui/experimental/mosaic/styles.css`:
193193

194194
```css
195-
@import '@clerk/ui/styles.css' layer(components);
195+
@import '@clerk/ui/experimental/mosaic/styles.css' layer(components);
196196

197197
@layer overrides {
198198
.cl-popover-popup[data-size='lg'] {

packages/ui/package.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,12 @@
5757
"default": "./dist/experimental/index.js"
5858
},
5959
"./themes/shadcn.css": "./dist/themes/shadcn.css",
60-
"./styles.css": {
60+
"./experimental/mosaic": {
61+
"types": "./dist-mosaic/index.d.ts",
62+
"import": "./dist-mosaic/index.js",
63+
"default": "./dist-mosaic/index.js"
64+
},
65+
"./experimental/mosaic/styles.css": {
6166
"types": "./styles.css.d.ts",
6267
"default": "./dist-mosaic/styles.css"
6368
},
@@ -81,14 +86,15 @@
8186
"register"
8287
],
8388
"scripts": {
84-
"build": "pnpm build:umd && pnpm build:esm && pnpm build:mosaic && pnpm check:no-rhc && pnpm type-check",
89+
"build": "pnpm build:umd && pnpm build:esm && pnpm build:mosaic && pnpm check:no-rhc && pnpm check:no-emotion && pnpm type-check",
8590
"build:analyze": "rspack build --config rspack.config.js --env production --env analyze",
8691
"build:esm": "tsdown",
8792
"build:mosaic": "tsdown --config tsdown.mosaic.config.mts",
8893
"build:rsdoctor": "RSDOCTOR=true rspack build --config rspack.config.js --env production",
8994
"build:umd": "rspack build --config rspack.config.js --env production",
9095
"bundlewatch": "FORCE_COLOR=1 bundlewatch --config bundlewatch.config.json",
9196
"bundlewatch:fix": "node bundlewatch-fix.mjs",
97+
"check:no-emotion": "node scripts/check-mosaic-emotion-free.mjs",
9298
"check:no-rhc": "node ../../scripts/search-for-rhc.mjs directory dist/no-rhc",
9399
"clean": "rimraf ./dist",
94100
"dev": "rspack serve --config rspack.config.js",
@@ -107,6 +113,7 @@
107113
"type-check": "tsc --noEmit"
108114
},
109115
"dependencies": {
116+
"@clerk/headless": "workspace:^",
110117
"@clerk/localizations": "workspace:^",
111118
"@clerk/shared": "workspace:^",
112119
"@emotion/cache": "11.11.0",
@@ -126,7 +133,6 @@
126133
"qrcode.react": "4.2.0"
127134
},
128135
"devDependencies": {
129-
"@clerk/headless": "workspace:^",
130136
"@floating-ui/react-dom": "^2.1.8",
131137
"@rsdoctor/rspack-plugin": "^1.5.9",
132138
"@rspack/cli": "catalog:rspack",

0 commit comments

Comments
 (0)