# Smaller bundles, subpath imports, and a required stylesheet

**Version:** v4.0.0

**Published:** August 10, 2026

`@sanity/ui` v4.0.0 is available. The theme of the release is bundle size: heavy components now live behind subpath entry points, unused components tree-shake out, and styles ship as a stylesheet you import yourself. Alongside that, a batch of long-deprecated props, hooks, and components are gone, and the package is ESM-only, requiring Node.js 22.12 or later and React 19.2 or later.

Follow the [@sanity/ui v3 to v4 migration guide](https://github.com/sanity-io/ui/blob/main/MIGRATION.md#sanityui-from-v3-to-v4) to upgrade.

## ESM-only, with new Node.js, React, and TypeScript requirements

`@sanity/ui` v4 is now an ESM-only package. CommonJS consumers are no longer supported. Your project must run Node.js 22.12 or later and React 19.2 or later. If you are on an older version of either, upgrade those dependencies before installing v4.

v4 raises three requirements:

- Node.js 22.12 or later.
- React 19.2 or later. `styled-components` remains a peer dependency at `^5.2 || ^6`.
- TypeScript `moduleResolution` set to `node16`, `nodenext`, or `bundler`. v4 resolves all types through `exports` and no longer ships `typesVersions`, so the legacy `node10` mode cannot resolve the subpath types on TypeScript 5.x, and is removed outright in TypeScript 7.

## A stylesheet import is now required

Styles are no longer injected automatically. Import `@sanity/ui/styles.css` in your application entry point, before any code that renders `@sanity/ui` components. Step 2 of the migration guide below shows the import.

This is what makes the bundle-size work possible. Components no longer carry `displayName` side effects, so the ones you don't use tree-shake out of your bundle, and `SrOnly` and `Spinner` no longer use styled-components at all. Without the stylesheet, those components render unstyled. More components move off runtime CSS-in-JS across the 4.x minors.

## Heavy components moved to subpath entry points

Several heavier components have been moved out of the main package entry point and into dedicated subpath imports. This change significantly reduces bundle size for consumers who do not use these components. The affected components are now available at the following subpaths:

- `@sanity/ui/toast`: `Toast`, `ToastProvider`, `useToast`
- `@sanity/ui/popover`: `Popover`
- `@sanity/ui/tooltip`: `Tooltip`, `TooltipDelayGroupProvider`, `TooltipDelayGroupContext`, `useTooltipDelayGroup`
- `@sanity/ui/menu`: `Menu`, `MenuButton`, `MenuDivider`, `MenuGroup`, `MenuItem`
- `@sanity/ui/autocomplete`: `Autocomplete`
- `@sanity/ui/breadcrumbs`: `Breadcrumbs`
- `@sanity/ui/code`: `Code`

The `@sanity/ui/theme` entry point is unchanged from v3 — theme APIs did not move.

## Deprecated props, hooks, and components removed

Long-deprecated APIs are removed in v4, but they don't disappear cleanly. Removed props are typed `never` and ignored at runtime, so passing one produces the TypeScript error `Type 'number' is not assignable to type 'undefined'` rather than an unknown-prop error. Removed hooks and components are still exported and throw when called, for example `Error: `ConditionalWrapper` was removed in @sanity/ui v4. Inline the conditional wrapping logic instead.` Expect these messages rather than a missing-export error.

- `space`: renamed to `gap` on `Button`, `Hotkeys`, `Inline`, `Stack`, `Select`, `TextInput`, `Tree`, `TreeItem`, `Breadcrumbs`, `MenuGroup`, and `MenuItem`. `Flex` and `Grid` never accepted `space`; they have always used `gap`.
- `Grid`: `columns` and `rows` are replaced by `gridTemplateColumns` and `gridTemplateRows`. The `column` and `row` prop families are replaced by their `gridColumn` and `gridRow` equivalents.
- `Menu`: `focusFirst` and `focusLast` are replaced by `shouldFocus`.
- `MenuButton`: the top-level popover props are replaced by a single `popover` object prop.
- `Popover`: `boundaryElement` is replaced by `floatingBoundary` and `referenceBoundary`. Use `BoundaryElementProvider` for `constrainSize` and max-width behavior.
- `Tooltip`: `allowedAutoPlacements` is replaced by `fallbackPlacements`.
- `useClickOutside`: replaced by `useClickOutsideEvent`, which takes a function returning its elements and returns nothing, instead of taking an array and returning a ref setter.
- `useElementRect`: replaced by `useElementSize`, which returns `{content, border}` objects rather than a `DOMRect`. Callers reading `rect.width` move to `size?.content.width`.
- `useForwardedRef`: removed. `ref` is a regular prop in React 19.
- `useArrayProp`: removed.
- `ConditionalWrapper`: removed. Inline the conditional wrapping logic instead.

The most common change is the `space`-to-`gap` rename:

**Before**

```tsx
// Before
<Stack space={3}>
  <Text>Hello</Text>
  <Text>World</Text>
</Stack>
```

**After**

```tsx
// After
<Stack gap={3}>
  <Text>Hello</Text>
  <Text>World</Text>
</Stack>
```

## Tooltip and Popover keep closed content mounted

Tooltip and Popover now use React's `<Activity>` component to keep closed content mounted in the DOM but hidden, which avoids repeated mount and unmount work for content that is expensive to render. Most code needs no changes, but two cases do:

- Recursive content: a Popover whose `content` renders another Popover must gate the recursion on `open`. Without that gate, `<Activity>` renders an infinitely deep hidden tree and the page hangs.
- Tests and selectors: closed content is now present in the DOM. Tests that asserted a tooltip was absent, and CSS relying on `:empty` or descendant matching, need to check visibility instead of presence.

With `animate` enabled, hiding is deferred until the exit animation finishes.

## All components are plain function components

With React 19 semantics, `ref` is a regular prop and all `forwardRef` wrappers are removed. Pass `ref` directly to any `@sanity/ui` component. One consequence: components no longer satisfy `react-is` checks such as `isForwardRef`, so code that branches on those checks needs updating.

## The private _visual-editing entry point is removed

The `@sanity/ui/_visual-editing` entry point is removed. It was private and undocumented, and there is no public replacement. Importing it now fails with `ERR_PACKAGE_PATH_NOT_EXPORTED` and the message `Package subpath './_visual-editing' is not defined by "exports"`. Remove any direct imports before upgrading.

## The @juggle/resize-observer polyfill is removed

`@juggle/resize-observer` is no longer bundled, and `_ResizeObserver` is no longer exported. Modern browsers and Node.js 22 and later support `ResizeObserver` natively. If you target an environment that doesn't, install and import a polyfill yourself.

## Additional changes

- `ErrorBoundary` renders a plain `<pre><code>` element instead of the `Code` component, which keeps `react-refractor` out of the root dependency graph. Update styling that targeted the old markup.
- The `react-compiler-runtime` dependency is dropped. Components are compiled with React Compiler targeting React 19's built-in runtime.
- v3 is still maintained on the `v3` branch. Fixes ship under the `release-v3` npm dist-tag.

## Migration guide

Follow the [@sanity/ui v3 to v4 migration guide](https://github.com/sanity-io/ui/blob/main/MIGRATION.md#sanityui-from-v3-to-v4).

