refactor(ui): move static styled-components CSS to vanilla-extract - #2827
refactor(ui): move static styled-components CSS to vanilla-extract#2827stipsan wants to merge 28 commits into
Conversation
…xtract Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
…xtract Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
…nilla-extract Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
…rings Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
The arrow size is the only prop-driven value, so it becomes a CSS variable set inline through @vanilla-extract/dynamic's assignInlineVars, while the placement and sibling selectors stay static in arrow.css.ts. The svg child rules keep their exact selectors via globalStyle so no specificity changes. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
…to vanilla-extract LoadingBarProgress stays on styled-components: its theme-derived background-color has to beat Card's runtime background-color at equal specificity, which the static stylesheet cannot guarantee. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
The child rule keeps only max-width and box-sizing; display: inline-block and vertical-align: middle were duplicates of what Inline already sets on the same children. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
…xtract Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
ExpandButton stays on styled-components: its margin: -4px has to beat Button's runtime margin: 0 at equal specificity. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Key stays on styled-components: its display: block has to beat KBD's runtime display: inline-block at equal specificity. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
…extract Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
The six styled(Primitive) wrappers become classes on the primitives; the two display: flex overrides become the primitives' own display prop and flex: 1 the Flex prop, so no rule competes with Box's runtime display rule. The enter animation is a class applied when animate is set, replacing the $animate transient prop and its runtime keyframes. dialogStyle keeps only the theme-derived backdrop color. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
🦋 Changeset detectedLatest commit: ec9d50e The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
…anilla-extract The shared root rule moves to styles/input/textInput.css.ts: TextInput's Card root takes display="flex" and only the alignment class, TextArea's plain span carries both. InputRoot, the icon boxes and the clear button become classes. Prefix, Suffix and RightCard stay on styled-components because their radius and background overrides have to beat Card's runtime rules at equal specificity. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The Dialog migration drops previously-present min-height: 0 flexbox constraints, which can break scrolling/overflow behavior in the dialog content layout.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR continues the @sanity/ui migration away from runtime styled-components by moving static component CSS into colocated *.css.ts modules that get extracted into @sanity/ui/styles.css, reducing runtime style generation and styled-components surface area.
Changes:
- Replaced multiple static
styled(...)blocks (Menu, Tabs, VirtualList, Layer, Tooltip, Toast, Dialog, Autocomplete, Breadcrumbs, Hotkeys, Arrow, Button loading overlay) with vanilla-extract classes andclsxmerging. - Introduced new vanilla-extract style modules (
*.css.ts) and adjusted components to apply those classes (including globalStyle/vars where needed). - Added runtime deps used by the migrated components:
clsxand@vanilla-extract/dynamic.
File summaries
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks new runtime dependencies (clsx, @vanilla-extract/dynamic). |
| packages/ui/package.json | Adds clsx and @vanilla-extract/dynamic to @sanity/ui dependencies. |
| packages/ui/src/core/utils/virtualList/virtualList.tsx | Replaces static styled wrappers with vanilla-extract classes + clsx. |
| packages/ui/src/core/utils/virtualList/virtualList.css.ts | Adds extracted styles for VirtualList root and item wrapper. |
| packages/ui/src/core/utils/layer/layer.tsx | Replaces static position: relative styled wrapper with vanilla-extract + clsx. |
| packages/ui/src/core/utils/layer/layer.css.ts | Adds extracted position: relative layer style. |
| packages/ui/src/core/utils/arrow/arrow.tsx | Migrates arrow wrapper + path styles; uses assignInlineVars for size var. |
| packages/ui/src/core/utils/arrow/arrow.css.ts | Adds extracted arrow styles (selectors/globalStyle, CSS var, stroke/fill). |
| packages/ui/src/core/primitives/tooltip/tooltipCard.tsx | Applies vanilla-extract class for will-change and merges className. |
| packages/ui/src/core/primitives/tooltip/tooltip.tsx | Migrates pointer-events: none wrapper styling to vanilla-extract. |
| packages/ui/src/core/primitives/tooltip/tooltip.css.ts | Adds extracted tooltip layer/card styles. |
| packages/ui/src/core/primitives/button/button.tsx | Replaces static loading overlay styled div with vanilla-extract class. |
| packages/ui/src/core/primitives/button/button.css.ts | Adds extracted loading overlay styles. |
| packages/ui/src/core/components/toast/toastLayer.tsx | Replaces styled Grid wrapper with vanilla-extract class on Grid as="ul". |
| packages/ui/src/core/components/toast/toastLayer.css.ts | Adds extracted toast viewport positioning styles. |
| packages/ui/src/core/components/toast/toast.tsx | Replaces several styled wrappers with vanilla-extract classes + motion.create(Card). |
| packages/ui/src/core/components/toast/toast.css.ts | Adds extracted Toast/LoadingBar styles (incl. fallbacks). |
| packages/ui/src/core/components/toast/styles.ts | Removes static styled exports; keeps theme-dependent LoadingBarProgress on styled-components. |
| packages/ui/src/core/components/tab/tabList.tsx | Replaces styled Inline wrapper with vanilla-extract class + global child rule. |
| packages/ui/src/core/components/tab/tabList.css.ts | Adds extracted TabList styles (global child max-width/box-sizing). |
| packages/ui/src/core/components/tab/tab.tsx | Replaces styled Button wrapper with vanilla-extract class merge. |
| packages/ui/src/core/components/tab/tab.css.ts | Adds extracted max-width: 100% tab style. |
| packages/ui/src/core/components/menu/menuDivider.tsx | Replaces styled.hr with polymorphic component applying vanilla-extract class. |
| packages/ui/src/core/components/menu/menuDivider.ts | Removes the old styled-components-based MenuDivider implementation. |
| packages/ui/src/core/components/menu/menuDivider.css.ts | Adds extracted MenuDivider static styles. |
| packages/ui/src/core/components/menu/menu.tsx | Replaces styled Menu wrapper with vanilla-extract class merge. |
| packages/ui/src/core/components/menu/menu.css.ts | Adds extracted Menu static styles (outline, overflow). |
| packages/ui/src/core/components/hotkeys/hotkeys.tsx | Migrates static wrapper styles to vanilla-extract while keeping Key on styled-components. |
| packages/ui/src/core/components/hotkeys/hotkeys.css.ts | Adds extracted Hotkeys wrapper styles. |
| packages/ui/src/core/components/dialog/styles.ts | Removes animation/keyframe CSS from styled-components helper; keeps theme backdrop styling. |
| packages/ui/src/core/components/dialog/dialog.tsx | Replaces multiple static styled wrappers with vanilla-extract classes; keeps StyledDialog for theme-dependent styling. |
| packages/ui/src/core/components/dialog/dialog.css.ts | Adds extracted dialog layout + animation keyframes/styles. |
| packages/ui/src/core/components/breadcrumbs/breadcrumbs.tsx | Replaces styled <ol> with vanilla-extract class merge. |
| packages/ui/src/core/components/breadcrumbs/breadcrumbs.styles.ts | Removes styled Breadcrumbs wrapper; keeps ExpandButton on styled-components due to specificity needs. |
| packages/ui/src/core/components/breadcrumbs/breadcrumbs.css.ts | Adds extracted Breadcrumbs list styles. |
| packages/ui/src/core/components/autocomplete/autocomplete.tsx | Replaces styled wrapper + listbox styles with vanilla-extract classes/globalStyle. |
| packages/ui/src/core/components/autocomplete/autocomplete.styles.tsx | Removes the old styled-components autocomplete wrapper/listbox module. |
| packages/ui/src/core/components/autocomplete/autocomplete.css.ts | Adds extracted Autocomplete wrapper and listbox ul reset styles. |
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
packages/ui/src/core/components/dialog/dialog.css.ts:52
min-height: 0was previously applied to the dialog layout flex item; without it, theDialogContentflex child may not be able to shrink and itsoverflow: autoscrolling can break. RestoringminHeight: 0keeps the flex/scroll behavior consistent with the previous implementation.
export const dialogLayout = style({
width: '100%',
})
- Files reviewed: 37/38 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…anilla-extract treeItemRootStyle's selectors move to tree.css.ts via globalStyle with the same selector shapes; the styled li keeps only treeItemRootColorStyle, whose color variables read the theme. TreeItemBox stays: its padding-left is theme space times the tree level. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
…alog and TabList classes Box's flexItem base and Inline's child rule set the same values, so dropping them changed nothing rendered, but the explicit declarations document the flex-shrink and inline layout intent where a reader looks for them. Same values on both sides, so the duplicate cannot depend on stylesheet order. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
It’s a broad, cross-cutting styling refactor (cascade/order-sensitive UI changes) that warrants final human verification beyond static review.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
- Files reviewed: 45/46 changed files
- Comments generated: 0 new
- Review effort level: Lite
…o vanilla-extract The root and InitialsLabel stay on styled-components: the root sets its color variables from the color prop and the focus ring from the theme, and InitialsLabel's color: inherit has to beat Label's runtime color at equal specificity. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
… vanilla-extract The static root wrappers (and the visually hidden Switch input) become plain elements with classes. The input and representation rules keep reading the theme and stay on styled-components. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
The shimmer keyframes and the prefers-reduced-motion media rules are static, so they move to skeleton.css.ts; the animated and visible states become classes toggled by the props instead of $animated/$visible transient props. The styled Box keeps only the responsive radius. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
The styled div keeps only the responsive gap between avatars, which reads theme sizes and breakpoints. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The change set is a broad styling refactor across many primitives/components where subtle cascade/specificity regressions are hard to fully validate via static review alone.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
- Files reviewed: 63/64 changed files
- Comments generated: 1
- Review effort level: Lite
| selectors: { | ||
| '&:not([hidden])': { | ||
| display: 'inline-block', | ||
| }, | ||
| '&[data-read-only]': { | ||
| outline: '1px solid red', | ||
| }, | ||
| }, |
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
commit: |
There was a problem hiding this comment.
🔵 Needs a closer look
A read-only Radio currently renders a hard-coded red outline via radio.css.ts, which is inconsistent with other read-only controls and likely unintended.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
packages/ui/src/core/primitives/radio/radio.css.ts:12
- The radio root currently applies
outline: 1px solid redwheneverdata-read-onlyis present. This looks like a debug-style and is inconsistent with other read-only inputs (e.g. Select/Checkbox), and will visibly render a red outline for normal read-only radios.
- Files reviewed: 65/66 changed files
- Comments generated: 0 new
- Review effort level: Lite
Same cascade as before: the visible state declares opacity: 1 instead of leaving the initial value, so a consumer class on the element is treated the same way it was. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
dialog.css.ts uses a direct-child selector for animating DialogCard that won’t match the actual DOM structure (focus-trap siblings), so the intended zoom animation won’t run.
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
packages/ui/src/core/components/dialog/dialog.css.ts:34
[data-ui='DialogCard']is not a direct child of the dialog layer (there are focus-trap<div tabIndex={0} />siblings before/after it indialog.tsx), so this> [data-ui='DialogCard']selector won’t match and the zoom-in animation won’t run.
packages/ui/src/core/primitives/radio/radio.css.ts:12
- The
&[data-read-only]selector is dead code: the wrapper<div>never setsdata-read-only(it’s set on the<input>instead inradio.tsx), so this outline rule will never apply and is confusing to keep in the extracted CSS.
- Files reviewed: 65/66 changed files
- Comments generated: 0 new
- Review effort level: Lite
Why
Continues the styled-components to vanilla-extract migration started in #2523. This sweep covers every styled-components block in
packages/ui/srcwhose CSS can be extracted without changing specificity or relying on stylesheet insertion order.Blocks stay on styled-components when they read theme/props at runtime, generate theme-breakpoint media rules, form primitive bases whose runtime extension order is public behavior, or must override another runtime rule at equal specificity.
What changed
@vanilla-extract/dynamicfor Arrow's runtime size variable.Inventory moved from 139 styled-components constructs in 60 files to 78 constructs in 47 files. The remainder has a reviewed keep reason.
Specificity safety
The migration predicate requires that a static class does not compete with a styled-components declaration for the same property at equal specificity. Where a primitive owns a property, the component now uses that primitive's prop instead of adding a competing class. The full computed-style suite was also rerun with extracted stylesheets moved after the styled-components runtime tag.
Verification
pnpm lintpnpm knippnpm test— 106 testspnpm test:browser— 247 tests across 61 story filespnpm --filter @sanity/ui build— includes publint and verifies extracteddist/styles.cssmain, in normal stylesheet ordermainand this branchpackages/ui/srcExpected characterization deltas are limited to vanilla-extract's scoped keyframe names and time/randomness-dependent story values. Three very large theme stories fail identically on both branches (
theme--buildandtheme--canvascrash Chromium;theme--colortimes out). CI is green.