Skip to content

feat(ui): migrate Mosaic Heading and Text to StyleX#9244

Open
alexcarpenter wants to merge 4 commits into
mainfrom
carp/mosaic-typography-stylex
Open

feat(ui): migrate Mosaic Heading and Text to StyleX#9244
alexcarpenter wants to merge 4 commits into
mainfrom
carp/mosaic-typography-stylex

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Jul 24, 2026

Copy link
Copy Markdown
Member

Description

Moves Heading and Text off the Emotion slot-recipe engine onto StyleX, following the Button pilot. Same public contract as Button: a stable .cl-heading / .cl-text class plus data-size / data-intent attributes, with the consumer's className/style merged last so they win over the component's own atoms.

Shared typography atoms. Both components render the same size and intent axes, so those atoms live once in components/typography.styles.ts. Heading composes a weight on top; Text adds nothing.

One type scale instead of per-component tokens. The six --cl-text-label-* tokens are gone. label / label-sm were byte-identical to the new sm / xs steps, so everything that renders text now sizes against a single named scale (xs2xl), Button included.

'--cl-text-sm-size': '0.875rem',
'--cl-text-sm-leading': 'calc(1.25 / 0.875)',

Font weights are their own token group. Weight and size vary independently (a sm Text can be medium; a 2xl Heading is semibold), so they don't belong on the step scale:

'--cl-font-normal': '400',
'--cl-font-medium': '500',
'--cl-font-semibold': '600',
'--cl-font-bold': '700',

Behavior preserved. render, HeadingContext / TextContext (Card sets intent: 'mutedForeground'), and ref forwarding are unchanged. The render path stays gated behind useRender, with a JSX fallback rather than React.createElement, so an Emotion parent's css prop still gets processed instead of leaking to the DOM.

Call sites. The sx prop is dropped as it was for Button; the three call sites that used it move to style.

Also in here

  • tsdown.mosaic.config.mts marks @clerk/headless external. The bundled dts emit dropped the type modifier on import { RenderProp } and failed the build; that entry only exists to extract styles.css, so there's nothing to gain from inlining the primitives.
  • Swingset's <PropTable> appended an sx row unconditionally. StyleX components have no sx, so meta.styleEngine now selects the escape-hatch rows (sx for Emotion, className + style for StyleX). Button, Heading, and Text declare 'stylex'.
  • The Heading/Text stories describe their variant surface statically — StyleX has no runtime recipe to derive knobs from — mirroring what button.stories.tsx already does.

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:

Move both typography components off the Emotion slot-recipe engine onto
StyleX atoms, following the Button migration's contract: a stable
`.cl-heading`/`.cl-text` class plus `data-size`/`data-intent` attrs, with
the consumer's `className`/`style` merged last so they win.

The size and intent axes are identical across the two components, so the
atoms live once in a shared `typography.styles.ts`; Heading only adds its
weight on top and Text adds nothing.

Collapse the `--cl-text-label-*` tokens into one named step scale
(`xs` -> `2xl`) that every text-bearing component sizes against -- the
`label`/`label-sm` steps were byte-identical to `sm`/`xs`. Font weights
become their own `--cl-font-{normal,medium,semibold,bold}` group, since
weight and size vary independently.

`render`, the Heading/Text contexts, and ref forwarding are unchanged.
The `sx` prop is dropped as it was for Button; the three call sites that
used it move to `style`.
…engine

`PropTable` appended an `sx` row unconditionally, but StyleX components have
no `sx` -- they merge the consumer's `className`/`style` after their own
atoms. `meta.styleEngine` now picks the row set, and Button, Heading, and
Text declare `'stylex'`.
@changeset-bot

changeset-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d97f67c

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 24, 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 24, 2026 8:22pm
swingset Ready Ready Preview, Comment Jul 24, 2026 8:22pm

Request Review

@pkg-pr-new

pkg-pr-new Bot commented Jul 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

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

@clerk/backend

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

@clerk/chrome-extension

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

@clerk/clerk-js

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

@clerk/electron

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

@clerk/electron-passkeys

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

@clerk/eslint-plugin

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

@clerk/expo

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

@clerk/expo-google-signin

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

@clerk/expo-passkeys

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

@clerk/express

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

@clerk/fastify

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

@clerk/hono

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

@clerk/localizations

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

@clerk/nextjs

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

@clerk/nuxt

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

@clerk/react

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

@clerk/react-router

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

@clerk/shared

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

@clerk/tanstack-react-start

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

@clerk/testing

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

@clerk/ui

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

@clerk/upgrade

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

@clerk/vue

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

commit: 9c01700

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-24T20:23:52.492Z

Summary

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

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on d97f67c.

@coderabbitai

coderabbitai Bot commented Jul 24, 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: 8a9d4523-6e0a-4474-9219-e38ee802405d

📥 Commits

Reviewing files that changed from the base of the PR and between 9c01700 and d97f67c.

📒 Files selected for processing (7)
  • packages/swingset/src/components/PropTable.tsx
  • packages/swingset/src/stories/button.stories.tsx
  • packages/swingset/src/stories/heading.stories.tsx
  • packages/swingset/src/stories/text.stories.tsx
  • packages/ui/src/mosaic/components/heading/heading.styles.ts
  • packages/ui/src/mosaic/components/typography.styles.ts
  • packages/ui/src/mosaic/tokens.stylex.ts
🔗 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)
  • clerk/clerk-ios (auto-detected)
  • clerk/cli (auto-detected)
  • clerk/clerk-android (auto-detected)
💤 Files with no reviewable changes (6)
  • packages/ui/src/mosaic/components/heading/heading.styles.ts
  • packages/swingset/src/stories/heading.stories.tsx
  • packages/swingset/src/stories/button.stories.tsx
  • packages/ui/src/mosaic/components/typography.styles.ts
  • packages/swingset/src/components/PropTable.tsx
  • packages/swingset/src/stories/text.stories.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui/src/mosaic/tokens.stylex.ts

📝 Walkthrough

Walkthrough

Mosaic typography is migrated to StyleX through shared size, intent, and font-weight tokens plus new Heading and Text components with context, rendering overrides, ref forwarding, and tests. Story metadata and PropTable now support engine-specific styling props. Mosaic exports, organization views, Button styles, build externalization, release metadata, and repository guidance are updated accordingly.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 Mosaic Heading and Text to StyleX.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the migration and related updates.
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: 3

🤖 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 @.changeset/mosaic-typography-stylex.md:
- Around line 1-2: Replace the empty changeset frontmatter with a non-empty
changeset for the Mosaic package, specifying the appropriate version bump for
the affected package and adding a concise user-facing summary of the `@clerk/ui`
component behavior and customization changes.

In `@packages/ui/src/mosaic/components/heading/heading.test.tsx`:
- Around line 39-44: Add a test alongside the existing callback case in the
heading test suite that passes a React element directly to Heading’s render
prop, then verify the cloned element receives the heading children and merged
slot attributes such as the cl-heading class and data-size value. Use the
existing Heading test patterns and keep the callback coverage unchanged.

In `@packages/ui/src/mosaic/components/text/text.tsx`:
- Around line 20-24: Update Text and its ref typing so the component’s exposed
ref element matches the host selected by render, using the project’s established
polymorphic component pattern; alternatively restrict render to
paragraph-compatible output if polymorphism is unsupported. Preserve the default
<p> behavior and add a regression covering a render override to <span> with the
corresponding ref type.
🪄 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: 49ac35ae-0fc3-4b6d-b542-d765b92416af

📥 Commits

Reviewing files that changed from the base of the PR and between 9081534 and 9c01700.

📒 Files selected for processing (25)
  • .changeset/mosaic-typography-stylex.md
  • .claude/skills/clerk-monorepo/SKILL.md
  • AGENTS.md
  • packages/swingset/CLAUDE.md
  • packages/swingset/src/components/PropTable.tsx
  • packages/swingset/src/lib/types.ts
  • packages/swingset/src/stories/button.stories.tsx
  • packages/swingset/src/stories/heading.stories.tsx
  • packages/swingset/src/stories/text.stories.tsx
  • packages/ui/src/mosaic/components/button/button.styles.ts
  • packages/ui/src/mosaic/components/heading.tsx
  • packages/ui/src/mosaic/components/heading/heading.styles.ts
  • packages/ui/src/mosaic/components/heading/heading.test.tsx
  • packages/ui/src/mosaic/components/heading/heading.tsx
  • packages/ui/src/mosaic/components/heading/index.ts
  • packages/ui/src/mosaic/components/text.tsx
  • packages/ui/src/mosaic/components/text/index.ts
  • packages/ui/src/mosaic/components/text/text.test.tsx
  • packages/ui/src/mosaic/components/text/text.tsx
  • packages/ui/src/mosaic/components/typography.styles.ts
  • packages/ui/src/mosaic/organization/organization-profile-domains-section.view.tsx
  • packages/ui/src/mosaic/organization/organization-profile-members-panel.view.tsx
  • packages/ui/src/mosaic/styles/index.ts
  • packages/ui/src/mosaic/tokens.stylex.ts
  • packages/ui/tsdown.mosaic.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)
  • clerk/clerk-ios (auto-detected)
  • clerk/cli (auto-detected)
  • clerk/clerk-android (auto-detected)
💤 Files with no reviewable changes (2)
  • packages/ui/src/mosaic/components/heading.tsx
  • packages/ui/src/mosaic/components/text.tsx

Comment on lines +1 to +2
---
---

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a non-empty changeset for the Mosaic package change.

This suppresses release metadata despite changes to @clerk/ui component behavior and customization. Add the appropriate package bump and a user-facing summary; reserve empty changesets for tooling-only work. As per coding guidelines, “use pnpm changeset for package changes” and use an empty changeset only when there is no user-facing impact.

🤖 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 @.changeset/mosaic-typography-stylex.md around lines 1 - 2, Replace the empty
changeset frontmatter with a non-empty changeset for the Mosaic package,
specifying the appropriate version bump for the affected package and adding a
concise user-facing summary of the `@clerk/ui` component behavior and
customization changes.

Source: Coding guidelines

Comment on lines +39 to +44
it('renders a different element through the render prop, keeping the slot props', () => {
render(<Heading render={p => <h3 {...p} />}>Title</Heading>);
const heading = screen.getByRole('heading', { level: 3 });
expect(heading).toHaveClass('cl-heading');
expect(heading).toHaveAttribute('data-size', 'base');
});

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover the React-element render branch.

HeadingProps accepts both a callback and a ReactElement, but this only tests the callback form. Add a <Heading render={<h3 />}>…</Heading> case that verifies children and merged slot attributes reach the cloned element. As per coding guidelines, “Unit tests are required for all new functionality.”

🤖 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/components/heading/heading.test.tsx` around lines 39 -
44, Add a test alongside the existing callback case in the heading test suite
that passes a React element directly to Heading’s render prop, then verify the
cloned element receives the heading children and merged slot attributes such as
the cl-heading class and data-size value. Use the existing Heading test patterns
and keep the callback coverage unchanged.

Source: Coding guidelines

Comment on lines +20 to +24
* Themeable body copy. Renders a `<p>` by default, forwards refs, and supports
* `size` and `intent` variants. Pass `render` for inline copy (`<span>`).
*/
export const Text = React.forwardRef<HTMLParagraphElement, TextProps>(function MosaicText(rawProps, ref) {
const { size = 'sm', intent = 'primary', render, className, style, ...rest } = useContextProps(rawProps, TextContext);

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## file\n'
sed -n '1,220p' packages/ui/src/mosaic/components/text/text.tsx

printf '\n## nearby type declarations\n'
rg -n "type TextProps|interface TextProps|render" packages/ui/src/mosaic/components/text -S

Repository: clerk/javascript

Length of output: 3795


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## usages/tests\n'
rg -n "Text[^\\n]*ref|<Text|TextContext|render:" packages/ui/src -S

printf '\n## text-related tests\n'
fd -a "text.*(test|spec).*" packages/ui/src

Repository: clerk/javascript

Length of output: 39734


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## related primitives\n'
rg -n "forwardRef<.*HTMLParagraphElement|makeCustomizable|createVariants|elementDescriptor|render\\s*\\?" packages/ui/src/mosaic packages/ui/src -S

Repository: clerk/javascript

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' packages/ui/src/mosaic/components/text/text.tsx

Repository: clerk/javascript

Length of output: 1927


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "function useRender|const useRender|export .*useRender|useRender\\(" . -g '!**/dist/**' -g '!**/build/**' -g '!**/node_modules/**' -S | head -n 80

Repository: clerk/javascript

Length of output: 7491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' packages/ui/src/mosaic/components/text/text.test.tsx

Repository: clerk/javascript

Length of output: 2478


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,280p' packages/headless/src/utils/use-render.tsx

Repository: clerk/javascript

Length of output: 9028


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' packages/headless/src/utils/use-render.test.tsx

Repository: clerk/javascript

Length of output: 6974


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' packages/ui/src/mosaic/components/heading/heading.tsx

Repository: clerk/javascript

Length of output: 2040


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "ref|render prop|span" packages/ui/src/mosaic/components/text/text.test.tsx packages/headless/src/utils/use-render.test.tsx -S

Repository: clerk/javascript

Length of output: 3922


Make Text polymorphic when render swaps the host element.
render can return a <span>, but Text is still exposed as Ref<HTMLParagraphElement>, so callers get a paragraph ref type for a span at runtime. Either make the component generic over the rendered element or restrict render to preserve <p>. Add a regression for the override/ref path.

🤖 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/components/text/text.tsx` around lines 20 - 24, Update
Text and its ref typing so the component’s exposed ref element matches the host
selected by render, using the project’s established polymorphic component
pattern; alternatively restrict render to paragraph-compatible output if
polymorphism is unsupported. Preserve the default <p> behavior and add a
regression covering a render override to <span> with the corresponding ref type.

Source: Coding guidelines

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