> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Sanity UI

Keep your custom studio elements consistent with built-in UI components.

When you're creating new tools and custom inputs, it's important for your editor experience to make sure your customizations match the overall design of the studio. To create this consistency, you can use [the Sanity UI component library](https://sanity.io/ui) to create custom experiences without creating custom designs or adding custom CSS.

## Usage of Sanity UI

The Sanity UI package comes bundled for most studio usage, but if you're creating a plugin or tool, you'll want to install the package via NPM.

**npm**

```shell
npm install @sanity/ui
```

**pnpm**

```shell
pnpm add @sanity/ui
```

**yarn**

```shell
yarn add @sanity/ui
```

**bun**

```shell
bun add @sanity/ui
```

From there, you can import the various components into your custom inputs, tools, or widgets. For example, if you wish to apply a tooltip to a string input, you can create a custom input that uses the `Stack`, `Box`, and `TextInput` design primitives to create one with all the design elements of your studio built right in. 

```javascript
// /components/MyCustomStringInput.jsx
import React, {useCallback} from 'react'
import {Stack, Text, TextInput} from '@sanity/ui'
import {set, unset} from 'sanity'

export const MyCustomStringInput = (props) => {
  const {elementProps, onChange, value = ''} = props

  const handleChange = useCallback((event) => {
    const nextValue = event.currentTarget.value
    onChange(nextValue ? set(nextValue) : unset())
	}, [onChange])

  return (
    <Stack gap={2}>
      <TextInput
        {...elementProps}
        onChange={handleChange}
        value={value}
      />
      <Text>Characters: {value.length}</Text>
    </Stack>
  )
}
```

See this guide on [creating custom inputs and tools with Sanity UI](https://www.sanity.io/guides/your-first-input-component-for-sanity-studio-v3).

## Compatibility and versioning

Sanity UI follows semantic versioning, and that guarantee covers the package's documented API: component props, hooks, and refs. Breaking changes to those ship in a major release, and the removed API stays in the TypeScript types as a deprecation message naming its replacement.

The markup a component renders is not part of that contract. DOM structure and internal attributes such as `data-ui` and `data-testid` can change without a major version bump, so code that queries rendered elements or asserts on them can break on a routine upgrade. Depend on the props, hooks, and refs a component documents instead.

> [!WARNING]
> Closed tooltips and popovers stay in the DOM
> From Sanity UI v4, `Tooltip` and `Popover` keep their content mounted while closed, using React's `<Activity>` component to hide it with `display: none`. A check for the presence of an element finds content that isn't visible.
> In unit tests, assert on visibility rather than existence: `expect(screen.getByText('Tooltip content')).not.toBeVisible()` replaces `expect(screen.queryByText('Tooltip content')).not.toBeInTheDocument()`. End-to-end assertions change the same way, from `toHaveCount(0)` to `toBeHidden()`. Queries that skip inaccessible elements, such as `getByRole()`, need no change.

To react to clicks outside a component, including one rendered in a portal, pass element refs to the `useClickOutsideEvent` hook rather than looking elements up with `document.querySelector`. The hook tracks the elements the components render, so it keeps working when the markup changes. It replaces `useClickOutside`, which v4 removed.

**components/MyPopoverButton.tsx**

```tsx
import {Box, Button, Text, useClickOutsideEvent} from '@sanity/ui'
import {Popover} from '@sanity/ui/popover'
import {useCallback, useRef, useState} from 'react'

export function MyPopoverButton() {
  const [open, setOpen] = useState(false)
  const buttonRef = useRef<HTMLButtonElement | null>(null)
  const popoverRef = useRef<HTMLDivElement | null>(null)

  const handleClickOutside = useCallback(() => setOpen(false), [])

  // Pass refs to the rendered elements, including the portaled popover card.
  // Passing `false` while closed disables the listener.
  useClickOutsideEvent(open && handleClickOutside, () => [
    buttonRef.current,
    popoverRef.current,
  ])

  return (
    <Popover
      content={
        <Box padding={3}>
          <Text>Popover content</Text>
        </Box>
      }
      open={open}
      portal
      ref={popoverRef}
    >
      <Button onClick={() => setOpen((prev) => !prev)} ref={buttonRef} text="Toggle" />
    </Popover>
  )
}
```

## Full documentation and playground

Sanity UI comes with a full set of UI primitives that can be mixed, matched, and composed into many different design patterns. The full list of components can be found in [the official Sanity UI documentation](https://sanity.io/ui/docs). To get a better feel for creating design patterns, you can also experiment with all the components in this [interactive component playground](https://www.sanity.io/ui/arcade).



## Related changelog entries

Entries are listed newest first; each link points to the full entry as markdown. Follow one when you need to know what changed, when, or why — for example, to summarize recent updates, explain behavior that differs from older documentation, or check whether a fix has shipped.

- [Sanity Studio v6.9.2: Sanity New, Content Releases fixes, and Studio UI improvements](https://www.sanity.io/docs/changelog/studio-Ni45LjE.md) — August 11, 2026
- [Sanity UI v4.0.0: Smaller bundles, subpath imports, and a required stylesheet](https://www.sanity.io/docs/changelog/ff1da457-dce8-45b7-9431-3fce5265fa8b.md) — August 10, 2026
- [Sanity UI v2.0.0: Major visual update to Sanity UI](https://www.sanity.io/docs/changelog/b3568a9e-6a42-4061-bfd2-a6c38cf99452.md) — January 16, 2024