Usage in Sanity Studio
When building tools and custom inputs in your studio, it's important for your editor's experience to keep things consistent. With Sanity UI, you can import properly styled components directly into any new customizations you make.
The studio ships with a version of Sanity UI, so no need to install new dependencies, we can immediately import any component directly into our files.
To add Sanity UI to a custom input requires that we first set up a simple custom input. First, we need to create a schema document with the field we want to customize.
Let's make a string input that is very important for SEO. So important that we want to add a tooltip on top of the standard description.

export default {
name: 'category',
title: 'Category',
type: 'document',
fields: [
{
name: 'seoString',
title: 'Something really important for SEO',
type: 'string',
inputComponent: HoverInput
}
]
}
Next, we need to create this custom input component.
Let's start by just having it be a standard string field. To do this, we need a few additional tools from Sanity's bag of tricks to make the data sync properly and bring amazing features like Presence and Review Changes into the mix. We'll also import our first component from Sanity UI, the humble TextInput
.
import React from 'react'
// Important items to allow form fields to work properly and patch the dataset.
import {PatchEvent, set} from 'part:@sanity/form-builder/patch-event'
import FormField from 'part:@sanity/components/formfields/default'
// Import the TextInput from UI
import { TextInput } from '@sanity/ui'
const HoverInput = React.forwardRef((props, ref) => {
const { type, onChange } = props
return(
<FormField label={type.title} description={type.description}>
<TextInput
type="text"
ref={ref}
placeholder={type.placeholder}
value={props.value}
onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
/>
</FormField>
)
})
export default {
name: 'category',
title: 'Category',
type: 'document',
fields: [
{
name: 'seoString',
title: 'Something really important for SEO',
type: 'string',
inputComponent: HoverInput
}
]
}
Next, we need to add our tooltip. We'll do that by importing both the Tooltip
component, as well as other components to help build the visual design of the tip.
import React from 'react'
import {PatchEvent, set} from 'part:@sanity/form-builder/patch-event'
import FormField from 'part:@sanity/components/formfields/default'
import {
Tooltip,
Text,
Box,
TextInput
} from '@sanity/ui'
const HoverInput = React.forwardRef((props, ref) => {
const { type, onChange } = props
return(
<FormField label={type.title} description={type.description}>
<Tooltip
content={(
<Box padding={2}>
<Text>Important Text</Text>
</Box>
)}
placement="top"
>
<TextInput
type="text"
ref={ref}
placeholder={type.placeholder}
value={props.value}
onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
/>
</Tooltip>
</FormField>
)
})

And with that, we now have a tooltip appearing on hover for our input field. The tooltip will always read "Important Text." Let's make this dynamic for our new input.
We'll start by updating our schema with a new property: tipDescription
.
export default {
name: 'category',
title: 'Category',
type: 'document',
fields: [
{
name: 'seoString',
title: 'Something really important for SEO',
description: 'Don\'t forget to make it SEO friendly!',
type: 'string',
tipDescription: 'Hey! Seriously, make it SEO friendly!',
inputComponent: HoverInput
}
]
}
Then, we'll use that new property in in our Tooltip.
<Tooltip
content={(
<Box padding={2}>
<Text>{type.tipDescription}</Text>
</Box>
)}
placement="top"
>

Finally, if you want to be extra protective, make sure to add a conditional in case someone forgets to add the tipDescription
to their schema when using this. Check to make sure tipDescription
exists before rendering a blank tooltip.
const HoverInput = React.forwardRef((props, ref) => {
const { type, onChange } = props
return(
<FormField label={type.title} description={type.description}>
{type.tipDescription ?
<Tooltip
content={(
<Box padding={2}>
<Text>{type.tipDescription}</Text>
</Box>
)}
placement="top"
>
<TextInput
type="text"
ref={ref}
placeholder={type.placeholder}
value={props.value}
onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
/>
</Tooltip>
:
<TextInput
type="text"
ref={ref}
placeholder={type.placeholder}
value={props.value}
onChange={event => {onChange(PatchEvent.from(set(event.target.value)))}}
/>
}
</FormField>
)
})
The full source code for this input is available on GitHub.

Custom tools are where Sanity UI has a little more room to shine. With a full page to work with, it can be overwhelming to begin designing from scratch. Not to worry, with layout components and design primitives, we'll have a layout knocked out in no time.
In order to get started building a custom tool, you'll first need to scaffold the files you'll need. The easiest way to do this is to run the following command with the Sanity CLI:
sanity init plugin
The CLI asks what type of tool to make. For our use case, we'll select "Basic, empty tool." It will ask you to name your plugin. For this example, we'll create a comment moderation dashboard, though this could be used for any type of content submitted by external users. We'll call our plugin comment-moderation
.
This creates all the files we need. To hook this up to our studio, we'll need to add the plugin's name to our sanity.json
file under the plugins
array.
// ...
"plugins": [
"@sanity/base",
"@sanity/components",
"@sanity/default-layout",
"@sanity/default-login",
"@sanity/desk-tool",
"comment-moderation"
],
// ...
Now, we're ready to get started.
In MyTool.js
, we'll refactor the starter code to export a functional React component and import a few UI components we'll need to get started.
import React from 'react'
import { Container, Card, Grid, Heading } from '@sanity/ui'
import styles from './MyTool.css'
export default function MyTool() {
// Where our UI will render
return (
)
From here, we'll add some basic UI. Let's build out a top box, with a constrained width and a white background. We also will want a little text to describe what this tool does.
We can use the Container
component to specify our centered-width box, the Card
component to create a background and allow for padding and other styles, and the Heading
component to create an <h1>
on the page.
import React from 'react'
import { Container, Card, Grid, Heading } from '@sanity/ui'
import styles from './MyTool.css'
export default function MyTool() {
// Where our UI will render
return (
<Container width={3}>
<Card margin={3} padding={4}>
<Stack space={3}>
<Heading as="h1" size={5}>Comment Moderation Dashboard</Heading>
<Text as="p">Moderate your comments here. Each box shows the latest 5 from each group.</Text>
</Stack>
</Card>
</Container>
)
}

From here, we can create a box for unapproved comments and list those comments out.
import React from 'react'
import { Container, Card, Grid, Heading, Stack, Box, Flex,Text, Label, Switch } from '@sanity/ui'
import styles from './MyTool.css'
export default function MyTool() {
// Where our UI will render
return (
<Container width={3}>
<Card margin={3} padding={5} className={styles.container}>
<Heading marginBottom={1} size={5} as={"h1"}>Comment Moderation Dashboard</Heading>
<p>Moderate your comments here. Each box shows the latest 5 from each group.</p>
</Card>
<Card margin={3}>
<Card marginBottom={1} paddingX={4} paddingTop={4} borderBottom={1} paddingBottom={0}>
<Heading size={3} as={"h2"}>To be moderated</Heading>
<p>Please moderate these comments</p>
</Card>
<Stack as={'ul'}>
<Card borderBottom as={'li'} padding={4}>
<Grid columns={5} justify={'space-between'} align={'center'}>
<Box column={4}>
<Stack space={3}>
<Text size={2}>This is a super amazing comment, please approve it</Text>
<Text muted size={1}>By: Sanity - A post to be named later</Text>
</Stack>
</Box>
<Flex justify={'center'} align={'center'}>
<Stack space={3}>
<Label>Approved?</Label>
<Switch
checked={false}
indeterminate={true}
/>
</Stack>
</Flex>
</Grid>
</Card>
</Stack>
</Card>
</Container>
)
}

First, we'll create a new Card
with similar margins above. Inside, we'll create a place for the Card to have a title and description. By using Card
again, we'll get access to margin and padding, but also borders to create a section for this area and separate it from the rest.
Immediately after this nested Card
, we'll create a Stack
. The Stack operates as a set of stacked content, in this case, we'll use this as a ul
and it's children will be list items.
Each of these comments will take place inside of a Grid
component to comprise our layout. The grid will be set up with five columns and justify it's content with additional white space set to space-between
and its vertical alignment set to center
.
The comment text itself will exist inside a Box
generic container that will set the number of columns for this to take up. Then, another Stack
component to stack a little information about the comment. The text of the comment, author and post will be inside of the Text
component and we can control the size and the color with properties.
Since we left enough room in our Grid
, let's toss a Switch
component in to act as our "approval" toggle. Since unapproved comments will come through as neither True
nor False
, we'll use the switch's indeterminate
prop to properly center the toggle in between. An input without a label is a bit of a UX and accessibility issue, so, let's make sure to create a new stack and put both the switch and a new Label
component in there.
At this point, our switch is aligned to the top of our comment, which is OK, but could be better. Let's wrap our stack in a Flex
component to vertically center our components.
This shows unapproved comments, but let's take this area and make two more to show approved comments and rejected comments. First, let's componentize this code to clean things up a bit.
import React from 'react'
import { Container, Card, Grid, Heading, Stack, Box, Flex,Text, Label, Switch } from '@sanity/ui'
import styles from './MyTool.css'
function CommentCard() {
return (
... ENTIRE CARD COMPONENT FROM BEFORE
)
}
export default function MyTool() {
// Where our UI will render
return (
<Container width={3}>
<Card margin={3} padding={5} className={styles.container}>
<Heading marginBottom={1} size={5} as={"h1"}>Comment Moderation Dashboard</Heading>
<p>Moderate your comments here. Each box shows the latest 5 from each group.</p>
</Card>
<CommentCard />
<CommentCard />
<CommentCard />
</Container>
)
}

This creates three identical stacked cards. Let's add some visual differences and have our approved and rejected boxes take up less visual space than our moderation queue.
To do this, we'll wrap our new CommentCard
components in a Grid
set to two columns.
<Grid columns={2}>
<CommentCard />
<CommentCard />
<CommentCard />
</Grid>

This gets our cards aligned in two columns, but we want our moderation queue to be full width. Let's add a prop to our custom component and have out card component go full width if it's present.
While we're at it, let's also add in a customizable title, description, and approval status.
import React from 'react'
import { Container, Card, Grid, Heading, Stack, Box, Flex,Text, Label, Switch } from '@sanity/ui'
import styles from './MyTool.css'
function CommentCard(props) {
const { title, description, approvalStatus, fullWidth } = props
return (
<Card column={fullWidth ? 'full' : ''} margin={3}>
<Card marginBottom={1} paddingX={4} paddingTop={4} borderBottom={1} paddingBottom={0}>
<Heading size={3} as={"h2"}>{title}</Heading>
<p>{description}</p>
</Card>
<Stack as={'ul'}>
<Card radius={2} borderBottom as={'li'} padding={4}>
<Grid columns={5} justify={'space-between'} align={'center'}>
<Box column={4}>
<Stack flex={1} space={3}>
<Text size={2}>This is a super amazing comment, please approve it</Text>
<Text muted size={1}>By: Sanity - A post to be named later</Text>
</Stack>
</Box>
<Flex justify={'center'} align={'center'}>
<Stack space={3}>
<Label>Approved?</Label>
<Switch
checked={approvalStatus}
indeterminate={(approvalStatus === undefined) ? true : false}
/>
</Stack>
</Flex>
</Grid>
</Card>
</Stack>
</Card>
)
}
export default function MyTool() {
// Where our UI will render
return (
<Container width={3}>
<Card margin={3} padding={5} className={styles.container}>
<Heading marginBottom={1} size={5} as={"h1"}>Comment Moderation Dashboard</Heading>
<p>Moderate your comments here. Each box shows the latest 5 from each group.</p>
</Card>
<Grid columns={2}>
<CommentCard fullWidth title="To Be Moderated" description="Moderate these please" approvalStatus={undefined} />
<CommentCard title="Approved" description="These are the good ones" approvalStatus={true} />
<CommentCard title="Unapproved" description="These are the bad ones" approvalStatus={false} />
</Grid>
</Container>
)
}

From here, it's a matter of pulling in live data and issuing patches in an onChange
event on each comment. See the full source code in this project created from this Next.js starter. See the UI full source here.
With just a few building blocks, you can create consistent, complex UI for your studio.
- Look through all the components
- Play in the interactive code arcade
- Create your first custom input or tool and give us feedback in the #beta channel of Sanity's Slack community