From 4b7f0d4ca72d14f07125a8e414e09edd96fc0a9f Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 4 Aug 2026 15:19:03 -0400 Subject: [PATCH 1/2] fix(ui): Give each Mosaic component a minimal CSS reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The injected reset in MosaicProvider is keyed on `data-cl-slot`, which only the slot-recipe components emit. The StyleX components style through `themeProps` + `stylex.props` and never carried it, so they picked up user-agent defaults — most visibly `` and ``, which rendered with the browser's paragraph and heading margins. The StyleX tier also builds through the Emotion-free `styles/index.ts` barrel, so it has no provider to inject anything and needs the reset as a StyleX atom rather than a stylesheet. `reset.base` is composed first in every `stylex.props(...)` call, mirroring how `typography.styles.ts` shares `sizes`/`colors`. `margin`/`padding` stay shorthands so component longhands outrank them by StyleX's own priority layers; the `inherit` declarations rely on the reset going first. --- .changeset/mosaic-component-reset.md | 7 ++ .../mosaic/components/avatar/avatar.styles.ts | 3 - .../src/mosaic/components/avatar/avatar.tsx | 7 +- .../mosaic/components/badge/badge.styles.ts | 2 - .../ui/src/mosaic/components/badge/badge.tsx | 8 +- .../mosaic/components/button/button.styles.ts | 2 - .../src/mosaic/components/button/button.tsx | 4 +- .../src/mosaic/components/card/card.styles.ts | 1 - .../ui/src/mosaic/components/card/card.tsx | 14 +++- .../src/mosaic/components/heading/heading.tsx | 3 +- .../ui/src/mosaic/components/icon/icon.tsx | 3 +- .../src/mosaic/components/item/item.styles.ts | 4 - .../ui/src/mosaic/components/item/item.tsx | 24 +++--- .../src/mosaic/components/menu/menu.styles.ts | 4 - .../ui/src/mosaic/components/menu/menu.tsx | 11 ++- .../src/mosaic/components/popover/popover.tsx | 5 +- .../ui/src/mosaic/components/reset.styles.ts | 21 +++++ .../ui/src/mosaic/components/reset.test.tsx | 81 +++++++++++++++++++ .../ui/src/mosaic/components/text/text.tsx | 8 +- 19 files changed, 169 insertions(+), 43 deletions(-) create mode 100644 .changeset/mosaic-component-reset.md create mode 100644 packages/ui/src/mosaic/components/reset.styles.ts create mode 100644 packages/ui/src/mosaic/components/reset.test.tsx diff --git a/.changeset/mosaic-component-reset.md b/.changeset/mosaic-component-reset.md new file mode 100644 index 00000000000..346c5c910c4 --- /dev/null +++ b/.changeset/mosaic-component-reset.md @@ -0,0 +1,7 @@ +--- +'@clerk/ui': patch +--- + +Mosaic components now carry a minimal reset, so they no longer inherit user-agent margins, padding, or fonts. Previously only the components built on slot recipes were reset; the rest picked up browser defaults, most visibly `` and ``, which rendered with the browser's paragraph and heading margins. + +Text and headings now sit flush with their container. A layout that relied on the browser's default spacing around them needs to set that spacing explicitly. diff --git a/packages/ui/src/mosaic/components/avatar/avatar.styles.ts b/packages/ui/src/mosaic/components/avatar/avatar.styles.ts index bda2beb9b1f..610dfc50370 100644 --- a/packages/ui/src/mosaic/components/avatar/avatar.styles.ts +++ b/packages/ui/src/mosaic/components/avatar/avatar.styles.ts @@ -8,10 +8,8 @@ export const styles = stylex.create({ overflow: 'hidden', alignItems: 'center', aspectRatio: '1 / 1', - boxSizing: 'border-box', display: 'inline-flex', flexShrink: 0, - fontFamily: 'inherit', fontWeight: fontWeightVars['--cl-font-medium'], justifyContent: 'center', lineHeight: 1, @@ -36,7 +34,6 @@ export const styles = stylex.create({ backgroundColor: `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 8%, transparent)`, color: colorVars['--cl-color-neutral'], display: 'flex', - fontSize: 'inherit', justifyContent: 'center', height: '100%', width: '100%', diff --git a/packages/ui/src/mosaic/components/avatar/avatar.tsx b/packages/ui/src/mosaic/components/avatar/avatar.tsx index ab997c724a4..89776eb127d 100644 --- a/packages/ui/src/mosaic/components/avatar/avatar.tsx +++ b/packages/ui/src/mosaic/components/avatar/avatar.tsx @@ -2,6 +2,7 @@ import * as stylex from '@stylexjs/stylex'; import React from 'react'; import { mergeStyleProps, themeProps } from '../../props'; +import { reset } from '../reset.styles'; import { shapes, sizes, styles } from './avatar.styles'; type ImageLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error'; @@ -39,7 +40,7 @@ const AvatarRoot = React.forwardRef(function Mosai ref={ref} {...mergeStyleProps( themeProps('avatar', { shape, size }), - stylex.props(styles.base, shapes[shape], sizes[size]), + stylex.props(reset.base, styles.base, shapes[shape], sizes[size]), className, style, )} @@ -87,7 +88,7 @@ const AvatarImage = React.forwardRef(functio ref={ref} src={src} alt={alt} - {...mergeStyleProps(themeProps('avatar-image'), stylex.props(styles.image), className, style)} + {...mergeStyleProps(themeProps('avatar-image'), stylex.props(reset.base, styles.image), className, style)} {...rest} /> ); @@ -120,7 +121,7 @@ const AvatarFallback = React.forwardRef(fu return ( {children} diff --git a/packages/ui/src/mosaic/components/badge/badge.styles.ts b/packages/ui/src/mosaic/components/badge/badge.styles.ts index 8b380c40d4d..9051d7df3b8 100644 --- a/packages/ui/src/mosaic/components/badge/badge.styles.ts +++ b/packages/ui/src/mosaic/components/badge/badge.styles.ts @@ -10,9 +10,7 @@ export const styles = stylex.create({ gap: space['1'], paddingInline: space['2'], alignItems: 'center', - boxSizing: 'border-box', display: 'inline-flex', - fontFamily: 'inherit', fontSize: typeScaleVars['--cl-text-xs-size'], fontWeight: fontWeightVars['--cl-font-medium'], justifyContent: 'center', diff --git a/packages/ui/src/mosaic/components/badge/badge.tsx b/packages/ui/src/mosaic/components/badge/badge.tsx index b689c778db2..51d7182896a 100644 --- a/packages/ui/src/mosaic/components/badge/badge.tsx +++ b/packages/ui/src/mosaic/components/badge/badge.tsx @@ -4,6 +4,7 @@ import React from 'react'; import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; +import { reset } from '../reset.styles'; import { colors, styles } from './badge.styles'; export type BadgeProps = MosaicComponentProps<'span'> & { @@ -37,7 +38,12 @@ export const Badge = React.forwardRef(function Mosa render, ref, props: { - ...mergeStyleProps(themeProps('badge', { color }), stylex.props(styles.base, colors[color]), className, style), + ...mergeStyleProps( + themeProps('badge', { color }), + stylex.props(reset.base, styles.base, colors[color]), + className, + style, + ), ...rest, }, }); diff --git a/packages/ui/src/mosaic/components/button/button.styles.ts b/packages/ui/src/mosaic/components/button/button.styles.ts index 0a8239c21d4..536fa32db13 100644 --- a/packages/ui/src/mosaic/components/button/button.styles.ts +++ b/packages/ui/src/mosaic/components/button/button.styles.ts @@ -84,12 +84,10 @@ export const styles = stylex.create({ alignItems: 'center', // Strips UA control styling so what's below is the whole appearance, not an override. appearance: 'none', - boxSizing: 'border-box', cursor: 'pointer', display: 'inline-flex', // A button is sized by its own axis, not by whatever row it lands in. flexShrink: 0, - fontFamily: 'inherit', fontWeight: fontWeightVars['--cl-font-medium'], justifyContent: 'center', outlineOffset: '2px', diff --git a/packages/ui/src/mosaic/components/button/button.tsx b/packages/ui/src/mosaic/components/button/button.tsx index 7906e9554b7..83b4bd8b4bb 100644 --- a/packages/ui/src/mosaic/components/button/button.tsx +++ b/packages/ui/src/mosaic/components/button/button.tsx @@ -3,6 +3,7 @@ import React from 'react'; import type { MosaicElementProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; +import { reset } from '../reset.styles'; import { truncationStyles } from '../typography.styles'; import { iconSizes, sizes, styles, variants } from './button.styles'; @@ -59,7 +60,7 @@ function withTruncatableLabel(children: React.ReactNode): React.ReactNode { result.push( {run} , @@ -108,6 +109,7 @@ export const Button = React.forwardRef(function {...mergeStyleProps( themeProps('button', { color, variant, size, shape, fullWidth, disabled }), stylex.props( + reset.base, styles.base, sizes[size], variants[`${variant}-${color}`], diff --git a/packages/ui/src/mosaic/components/card/card.styles.ts b/packages/ui/src/mosaic/components/card/card.styles.ts index 035231712d8..b646125bf40 100644 --- a/packages/ui/src/mosaic/components/card/card.styles.ts +++ b/packages/ui/src/mosaic/components/card/card.styles.ts @@ -27,7 +27,6 @@ export const styles = stylex.create({ paddingBlock: space['4'], paddingInline: space['6'], alignItems: 'center', - boxSizing: 'border-box', display: 'flex', flexShrink: 0, justifyContent: 'space-between', diff --git a/packages/ui/src/mosaic/components/card/card.tsx b/packages/ui/src/mosaic/components/card/card.tsx index 134aca83714..506e3f43db9 100644 --- a/packages/ui/src/mosaic/components/card/card.tsx +++ b/packages/ui/src/mosaic/components/card/card.tsx @@ -4,6 +4,7 @@ import React from 'react'; import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; +import { reset } from '../reset.styles'; import { TextContext } from '../text'; import { elevations, headerAlignments, styles } from './card.styles'; @@ -37,7 +38,7 @@ const Root = React.forwardRef(function CardRoot( props: { ...mergeStyleProps( themeProps('card-root', { alignment, elevation }), - stylex.props(styles.root, elevations[elevation]), + stylex.props(reset.base, styles.root, elevations[elevation]), className, style, ), @@ -60,7 +61,7 @@ const Header = React.forwardRef>(fun props: { ...mergeStyleProps( themeProps('card-header', { alignment }), - stylex.props(styles.header, headerAlignments[alignment]), + stylex.props(reset.base, styles.header, headerAlignments[alignment]), className, style, ), @@ -80,7 +81,7 @@ const Content = React.forwardRef>(fu render, ref, props: { - ...mergeStyleProps(themeProps('card-content'), stylex.props(styles.content), className, style), + ...mergeStyleProps(themeProps('card-content'), stylex.props(reset.base, styles.content), className, style), ...rest, }, }); @@ -96,7 +97,12 @@ const Footer = React.forwardRef>(fun render, ref, props: { - ...mergeStyleProps(themeProps('card-footer', { elevation }), stylex.props(styles.footer), className, style), + ...mergeStyleProps( + themeProps('card-footer', { elevation }), + stylex.props(reset.base, styles.footer), + className, + style, + ), ...rest, }, }); diff --git a/packages/ui/src/mosaic/components/heading/heading.tsx b/packages/ui/src/mosaic/components/heading/heading.tsx index dfb97a4d299..365a1c4a84c 100644 --- a/packages/ui/src/mosaic/components/heading/heading.tsx +++ b/packages/ui/src/mosaic/components/heading/heading.tsx @@ -5,6 +5,7 @@ import React from 'react'; import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { useContextProps } from '../../utils/context'; +import { reset } from '../reset.styles'; import type { TypographyColor, TypographySize } from '../typography.styles'; import { colors, sizes } from '../typography.styles'; import { styles } from './heading.styles'; @@ -33,7 +34,7 @@ export const Heading = React.forwardRef(functi const props = { ...mergeStyleProps( themeProps('heading', { size, color }), - stylex.props(styles.base, sizes[size], colors[color]), + stylex.props(reset.base, styles.base, sizes[size], colors[color]), className, style, ), diff --git a/packages/ui/src/mosaic/components/icon/icon.tsx b/packages/ui/src/mosaic/components/icon/icon.tsx index 4323053f184..b5f756068b4 100644 --- a/packages/ui/src/mosaic/components/icon/icon.tsx +++ b/packages/ui/src/mosaic/components/icon/icon.tsx @@ -5,6 +5,7 @@ import { useMosaicIcons } from '../../appearance'; import type { IconName } from '../../icons/registry'; import { iconRegistry } from '../../icons/registry'; import { mergeStyleProps, themeProps } from '../../props'; +import { reset } from '../reset.styles'; import { iconScope } from './icon.markers.stylex'; import { sizes, styles } from './icon.styles'; @@ -33,7 +34,7 @@ export const Icon = React.forwardRef(function MosaicIc // child by what it is: `:has([data-icon='inline-end'])` can't match some other placed descendant. const props = mergeStyleProps( themeProps('icon', { size, icon: placement }), - stylex.props(styles.base, sizes[size], iconScope), + stylex.props(reset.base, styles.base, sizes[size], iconScope), className, style, ); diff --git a/packages/ui/src/mosaic/components/item/item.styles.ts b/packages/ui/src/mosaic/components/item/item.styles.ts index bb787fc74a3..7f8af072155 100644 --- a/packages/ui/src/mosaic/components/item/item.styles.ts +++ b/packages/ui/src/mosaic/components/item/item.styles.ts @@ -5,8 +5,6 @@ import { itemScope } from './item.markers.stylex'; export const item = stylex.create({ base: { - margin: 0, - padding: 0, borderRadius: radiusVars['--cl-radius-element'], outline: { default: 'none', @@ -14,10 +12,8 @@ export const item = stylex.create({ }, paddingInline: space['2'], alignItems: 'center', - boxSizing: 'border-box', color: colorVars['--cl-color-card-foreground'], display: 'flex', - fontFamily: 'inherit', fontSize: typeScaleVars['--cl-text-sm-size'], lineHeight: typeScaleVars['--cl-text-sm-leading'], outlineOffset: '2px', diff --git a/packages/ui/src/mosaic/components/item/item.tsx b/packages/ui/src/mosaic/components/item/item.tsx index 2b93ccc66e7..b783cf65bd6 100644 --- a/packages/ui/src/mosaic/components/item/item.tsx +++ b/packages/ui/src/mosaic/components/item/item.tsx @@ -4,6 +4,7 @@ import React from 'react'; import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; +import { reset } from '../reset.styles'; import { truncationStyles } from '../typography.styles'; import { itemScope } from './item.markers.stylex'; import * as slots from './item.styles'; @@ -50,7 +51,7 @@ const Root = React.forwardRef(function MosaicItem( props: { ...mergeStyleProps( themeProps('item', { interactive, size }), - stylex.props(itemScope, slots.item.base, slots.item[size], interactive && slots.item.interactive), + stylex.props(reset.base, itemScope, slots.item.base, slots.item[size], interactive && slots.item.interactive), className, style, ), @@ -78,7 +79,7 @@ const Media = React.forwardRef>(func props: { ...mergeStyleProps( themeProps('item-media', { size }), - stylex.props(slots.media.base, slots.media[size]), + stylex.props(reset.base, slots.media.base, slots.media[size]), className, style, ), @@ -97,7 +98,7 @@ const Content = React.forwardRef>(fu render, ref, props: { - ...mergeStyleProps(themeProps('item-content'), stylex.props(slots.content.base), className, style), + ...mergeStyleProps(themeProps('item-content'), stylex.props(reset.base, slots.content.base), className, style), ...rest, }, }); @@ -115,7 +116,7 @@ const Title = React.forwardRef>(func props: { ...mergeStyleProps( themeProps('item-title'), - stylex.props(slots.title.base, truncationStyles.singleLine), + stylex.props(reset.base, slots.title.base, truncationStyles.singleLine), className, style, ), @@ -136,7 +137,7 @@ const Description = React.forwardRef props: { ...mergeStyleProps( themeProps('item-description'), - stylex.props(slots.description.base, truncationStyles.singleLine), + stylex.props(reset.base, slots.description.base, truncationStyles.singleLine), className, style, ), @@ -161,7 +162,7 @@ const Label = React.forwardRef>(func props: { ...mergeStyleProps( themeProps('item-label'), - stylex.props(slots.label.base, truncationStyles.singleLine), + stylex.props(reset.base, slots.label.base, truncationStyles.singleLine), className, style, ), @@ -180,7 +181,7 @@ const Actions = React.forwardRef>(fu render, ref, props: { - ...mergeStyleProps(themeProps('item-actions'), stylex.props(slots.actions.base), className, style), + ...mergeStyleProps(themeProps('item-actions'), stylex.props(reset.base, slots.actions.base), className, style), ...rest, }, }); @@ -196,7 +197,7 @@ const Group = React.forwardRef>(func render, ref, props: { - ...mergeStyleProps(themeProps('item-group'), stylex.props(slots.group.base), className, style), + ...mergeStyleProps(themeProps('item-group'), stylex.props(reset.base, slots.group.base), className, style), ...rest, }, }); @@ -212,7 +213,12 @@ const Separator = React.forwardRef>(fu render, ref, props: { - ...mergeStyleProps(themeProps('item-separator'), stylex.props(slots.separator.base), className, style), + ...mergeStyleProps( + themeProps('item-separator'), + stylex.props(reset.base, slots.separator.base), + className, + style, + ), ...rest, }, }); diff --git a/packages/ui/src/mosaic/components/menu/menu.styles.ts b/packages/ui/src/mosaic/components/menu/menu.styles.ts index 82c60c8dfa0..78de2a5699b 100644 --- a/packages/ui/src/mosaic/components/menu/menu.styles.ts +++ b/packages/ui/src/mosaic/components/menu/menu.styles.ts @@ -20,7 +20,6 @@ export const styles = stylex.create({ boxShadow: `0 12px 12px -7px oklch(0.2046 0 0 / 12%), 0 24px 24px -10px oklch(0.2046 0 0 / 4%), 0 0 0 1px oklch(0.2046 0 0 / 4%)`, - boxSizing: 'border-box', color: colorVars['--cl-color-card-foreground'], display: 'flex', flexDirection: 'column', @@ -63,11 +62,8 @@ export const styles = stylex.create({ ':hover': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`, }, }, - boxSizing: 'border-box', - color: 'inherit', cursor: { default: 'pointer', ':is([data-disabled])': 'not-allowed' }, display: 'flex', - fontFamily: 'inherit', fontSize: typeScaleVars['--cl-text-sm-size'], fontWeight: fontWeightVars['--cl-font-medium'], lineHeight: typeScaleVars['--cl-text-sm-leading'], diff --git a/packages/ui/src/mosaic/components/menu/menu.tsx b/packages/ui/src/mosaic/components/menu/menu.tsx index a8aa0abbc80..ecff3266ff5 100644 --- a/packages/ui/src/mosaic/components/menu/menu.tsx +++ b/packages/ui/src/mosaic/components/menu/menu.tsx @@ -13,6 +13,7 @@ import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { Button } from '../button'; import { Icon } from '../icon'; +import { reset } from '../reset.styles'; import { styles } from './menu.styles'; export type { MenuProps, MenuSeparatorProps }; @@ -62,10 +63,12 @@ export const MenuContent = React.forwardRef(fu ) { return ( - + {children} @@ -91,7 +94,7 @@ export const MenuItem = React.forwardRef(funct label={label} {...mergeStyleProps( themeProps('menu-item', { color }), - stylex.props(styles.item, color === 'negative' && styles.itemNegative), + stylex.props(reset.base, styles.item, color === 'negative' && styles.itemNegative), className, style, )} @@ -106,7 +109,7 @@ export const MenuItem = React.forwardRef(funct export function MenuSeparator({ className, style, ...rest }: MenuSeparatorProps): React.ReactElement { return ( ); diff --git a/packages/ui/src/mosaic/components/popover/popover.tsx b/packages/ui/src/mosaic/components/popover/popover.tsx index 16bc7b01edf..ccd9079e41f 100644 --- a/packages/ui/src/mosaic/components/popover/popover.tsx +++ b/packages/ui/src/mosaic/components/popover/popover.tsx @@ -5,6 +5,7 @@ import React from 'react'; import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; +import { reset } from '../reset.styles'; import { sizes, styles } from './popover.styles'; export type PopoverSize = 'sm' | 'md' | 'lg'; @@ -123,7 +124,7 @@ function Positioner({ children, ...rest }: React.ComponentPropsWithoutRef {children} @@ -168,7 +169,7 @@ const Popup = React.forwardRef(function Popov ref={ref} {...mergeStyleProps( themeProps('popover-popup', { size }), - stylex.props(styles.popup, sizes[size]), + stylex.props(reset.base, styles.popup, sizes[size]), className, style, )} diff --git a/packages/ui/src/mosaic/components/reset.styles.ts b/packages/ui/src/mosaic/components/reset.styles.ts new file mode 100644 index 00000000000..b84b9b3fda5 --- /dev/null +++ b/packages/ui/src/mosaic/components/reset.styles.ts @@ -0,0 +1,21 @@ +import * as stylex from '@stylexjs/stylex'; + +// The minimal reset every Mosaic element composes, as the first atom of its `stylex.props(...)`. +// Carried per-element rather than injected as a stylesheet: the StyleX tier builds through the +// Emotion-free `styles/index.ts` barrel, which has no provider to inject anything. +// +// `margin`/`padding` are shorthands deliberately — StyleX ranks a longhand above a shorthand +// regardless of order, so a component's `paddingInline` wins without depending on argument order. +// The `inherit` longhands tie with a component's own value, so they rely on the reset going first. +export const reset = stylex.create({ + base: { + margin: 0, + padding: 0, + boxSizing: 'border-box', + color: 'inherit', + fontFamily: 'inherit', + fontSize: 'inherit', + fontWeight: 'inherit', + lineHeight: 'inherit', + }, +}); diff --git a/packages/ui/src/mosaic/components/reset.test.tsx b/packages/ui/src/mosaic/components/reset.test.tsx new file mode 100644 index 00000000000..9c9547f9113 --- /dev/null +++ b/packages/ui/src/mosaic/components/reset.test.tsx @@ -0,0 +1,81 @@ +import * as stylex from '@stylexjs/stylex'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it } from 'vitest'; + +import { Avatar } from './avatar'; +import { Badge } from './badge'; +import { Button } from './button'; +import { Card } from './card'; +import { Heading } from './heading'; +import { Icon } from './icon'; +import { Item } from './item'; +import { reset } from './reset.styles'; +import { Text } from './text'; + +// StyleX generates the same atom for the same property+value across separate `create` calls, so a +// local probe names the atoms to assert on without hardcoding hashes that a StyleX upgrade rewrites. +const probe = stylex.create({ + borderBox: { boxSizing: 'border-box' }, + inheritedWeight: { fontWeight: 'inherit' }, +}); + +const classes = (style: stylex.StyleXStyles) => (stylex.props(style).className ?? '').split(' ').filter(Boolean); +const atoms = (style: stylex.StyleXStyles) => classes(style).filter(name => !name.includes('__')); + +// StyleX drops an atom once a component sets the same property, which is the whole point of the +// reset going first — so `margin`/`padding`/the `inherit` declarations cannot be asserted on every +// element. Two things do hold everywhere: +// - `box-sizing`, the one property nothing overrides +// - the `__` marker class, which survives every property-level override and is +// therefore the actual proof that a component composed `reset.base` at all +const borderBoxAtom = atoms(probe.borderBox); +const resetMarker = classes(reset.base).filter(name => name.includes('__')); + +// Every Mosaic element that carries its own styles. A new component is only covered once it is +// listed here — that omission is the failure this suite exists to catch. +const cases: Array<[string, React.ReactElement]> = [ + ['Avatar', ], + ['Badge', Beta], + ['Button', ], + ['Card', ], + ['Card.Header', ], + ['Card.Content', ], + ['Card.Footer', ], + ['Heading', Title], + [ + 'Icon', + , + ], + ['Item', ], + ['Item.Group', ], + ['Item.Separator', ], + ['Text', Body copy], +]; + +describe('Mosaic reset', () => { + it('derives the classes it asserts on from the reset itself', () => { + expect(borderBoxAtom).toHaveLength(1); + expect(resetMarker).toHaveLength(1); + expect(classes(reset.base)).toEqual(expect.arrayContaining([...borderBoxAtom, ...resetMarker])); + }); + + it.each(cases)('%s carries the reset on its root element', (_name, ui) => { + const { container } = render(ui); + const element = container.firstElementChild; + + expect(element).not.toBeNull(); + expect(element).toHaveClass(...resetMarker, ...borderBoxAtom); + }); + + it('lets a component win over the reset it composes first', () => { + const { container } = render(Title); + + // `reset.base` sets `fontWeight: inherit`; `heading.styles` sets semibold after it, so StyleX + // must have dropped the reset's atom. Order-dependent, which is why the reset always goes first. + expect(container.firstElementChild).not.toHaveClass(...atoms(probe.inheritedWeight)); + }); +}); diff --git a/packages/ui/src/mosaic/components/text/text.tsx b/packages/ui/src/mosaic/components/text/text.tsx index e43bb7204ec..30e248b3508 100644 --- a/packages/ui/src/mosaic/components/text/text.tsx +++ b/packages/ui/src/mosaic/components/text/text.tsx @@ -5,6 +5,7 @@ import React from 'react'; import type { MosaicComponentProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { useContextProps } from '../../utils/context'; +import { reset } from '../reset.styles'; import type { TypographyColor, TypographySize } from '../typography.styles'; import { colors, sizes } from '../typography.styles'; @@ -23,7 +24,12 @@ export const Text = React.forwardRef(function M const { size = 'sm', color = 'primary', render, className, style, ...rest } = useContextProps(rawProps, TextContext); const props = { - ...mergeStyleProps(themeProps('text', { size, color }), stylex.props(sizes[size], colors[color]), className, style), + ...mergeStyleProps( + themeProps('text', { size, color }), + stylex.props(reset.base, sizes[size], colors[color]), + className, + style, + ), ...rest, }; From beb47e96e6098625f9b8cd9fefe7ca569b2e003c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 4 Aug 2026 15:20:07 -0400 Subject: [PATCH 2/2] Apply suggestion from @alexcarpenter --- .changeset/mosaic-component-reset.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.changeset/mosaic-component-reset.md b/.changeset/mosaic-component-reset.md index 346c5c910c4..a845151cc84 100644 --- a/.changeset/mosaic-component-reset.md +++ b/.changeset/mosaic-component-reset.md @@ -1,7 +1,2 @@ --- -'@clerk/ui': patch --- - -Mosaic components now carry a minimal reset, so they no longer inherit user-agent margins, padding, or fonts. Previously only the components built on slot recipes were reset; the rest picked up browser defaults, most visibly `` and ``, which rendered with the browser's paragraph and heading margins. - -Text and headings now sit flush with their container. A layout that relied on the browser's default spacing around them needs to set that spacing explicitly.