Skip to content

refactor(ui): move static styled-components CSS to vanilla-extract - #2827

Open
stipsan wants to merge 28 commits into
mainfrom
cursor/vanilla-extract-static-styles-sweep-f092
Open

refactor(ui): move static styled-components CSS to vanilla-extract#2827
stipsan wants to merge 28 commits into
mainfrom
cursor/vanilla-extract-static-styles-sweep-f092

Conversation

@stipsan

@stipsan stipsan commented Sep 4, 2026

Copy link
Copy Markdown
Member

Why

Continues the styled-components to vanilla-extract migration started in #2523. This sweep covers every styled-components block in packages/ui/src whose 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

  • Migrated static rules across Arrow, Toast, TabList, Autocomplete, Breadcrumbs, Hotkeys, Button, Tooltip, Dialog, TextInput/TextArea wrappers, Tree, Avatar/AvatarStack, Select, Switch, Checkbox, Radio, and Skeleton.
  • Included the MenuDivider, VirtualList, toast viewport, Menu, Layer, and Tab migrations from Migrate MenuDivider, VirtualList and the toast viewport to vanilla-extract #2732 and Migrate Menu, Layer and Tab to vanilla-extract #2736; this PR supersedes both.
  • Added @vanilla-extract/dynamic for Arrow's runtime size variable.
  • Added a minor changeset describing the extracted stylesheet requirement.
  • Preserved runtime/equal-specificity styles on styled-components with colocated rationale.

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 lint
  • pnpm knip
  • pnpm test — 106 tests
  • pnpm test:browser — 247 tests across 61 story files
  • pnpm --filter @sanity/ui build — includes publint and verifies extracted dist/styles.css
  • Computed-style comparison of 244 Storybook stories against pristine main, in normal stylesheet order
  • Like-for-like computed-style comparison with stylesheets swapped on both pristine main and this branch
  • 19 interactive Storybook scenarios covering open, hover, focus, loading, delayed, and prop-driven states
  • JSX key-order checker: 0 violations in packages/ui/src

Expected 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--build and theme--canvas crash Chromium; theme--color times out). CI is green.

Open in Web Open in Cursor 

cursoragent and others added 16 commits September 3, 2026 23:42
…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>
Copilot AI lite review requested due to automatic review settings September 4, 2026 00:06
@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ec9d50e

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

This PR includes changesets to release 2 packages
Name Type
@sanity/ui Minor
@sanity/themer Patch

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 Sep 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
icons Ready Ready Preview Sep 4, 2026 1:39am UTC
sanity-ui-storybook Ready Ready Preview Sep 4, 2026 1:39am UTC
ui-docs Ready Ready Preview Sep 4, 2026 1:39am UTC
1 Skipped Deployment
Project Deployment Actions Updated
sanity-ui-workshop Ignored Ignored Sep 4, 2026 1:39am UTC

Request Review

…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 and clsx merging.
  • 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: clsx and @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: 0 was previously applied to the dialog layout flex item; without it, the DialogContent flex child may not be able to shrink and its overflow: auto scrolling can break. Restoring minHeight: 0 keeps 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.

Comment thread packages/ui/src/core/components/dialog/dialog.css.ts
cursoragent and others added 3 commits September 4, 2026 00:12
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

cursoragent and others added 3 commits September 4, 2026 00:39
…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>
cursoragent and others added 2 commits September 4, 2026 00:45
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

Comment on lines +5 to +12
selectors: {
'&:not([hidden])': {
display: 'inline-block',
},
'&[data-read-only]': {
outline: '1px solid red',
},
},
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@sanity/color@2827
npm i https://pkg.pr.new/@sanity/icons@2827
npm i https://pkg.pr.new/@sanity/logos@2827
npm i https://pkg.pr.new/@sanity/ui@2827

commit: ec9d50e

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 red whenever data-read-only is 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

cursoragent and others added 2 commits September 4, 2026 01:36
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 in dialog.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 sets data-read-only (it’s set on the <input> instead in radio.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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants