👀 See Sanity in action: Watch product demo now →
December 13, 2022

How to build an input component for Sanity Studio v3

By Knut Melvær

This guide teaches how to add a custom input component to a field for Sanity Studio v3. It covers replacing a default input component with a custom React component, passing necessary properties into an input element, and patching real-time changes to the Content Lake from the input component.

This step-by-step guide will take you through the necessary steps of adding a custom input component to a field for Sanity Studio v3. You will be taken through 3 steps and learn the essential concepts you need to know in order to:

  • Replace a default input component with your own React component
  • Pass the necessary properties into an input element for accessibility and editor experience
  • Patch real-time changes to the Content Lake from your input component

You will do this by adding a custom string input field that includes a simple character count.

Completing the lesson should take you about 30–40 minutes. That doesn’t mean you should feel bad if it takes you longer. We all learn at different paces.

Open a Sanity Studio v3 in your favorite code editor and get started!

Protip

This guide is made for people who are new to custom input components for Sanity Studio. All code examples are copy-pasteable and should work if you follow the instructions closely.

Running a Sanity Studio project locally

This lesson assumes that you know how to get started with Sanity Studio and have basic knowledge about its project structure and schema files.

Basic HTML, CSS, and JavaScript

You could probably make it through this section without any preexisting knowledge of these languages, but it will be a lot easier if you understand the basic concepts. Mozilla Developer Network (MDN) has a great introduction to the basics of HTML, CSS, and JavaScript.

How to build with React

To build custom components for Sanity Studio, you will need to know basic React and JSX syntax. The lesson will provide you with all the code you need and explain how it works, so you can use it to sharpen your React skills too. You can check out the official docs (in beta) and Next.js’ introduction to React for an excellent introduction to React.


Overriding an input component

Sanity Studio v3 comes with a customization framework PIs that lets you override different parts of the content form. In this lesson, you will focus on the input component part of a field. This is the element in a field that makes it possible for someone to change content. Customizing an input field will not affect a field’s other properties, such as its title, description, validation, presence, and change indicators. If you want to customize any of these, you need to use the Field Component API.

The details of how to override a default input component can vary depending on the field’s schema type (for example, string, number, image, or object). It’s also possible to implement custom components that replace input fields globally with plugins. This lesson covers how to do it for a specific string schema field.

The following will assume that you have installed the blog project template in the CLI. You can, however, follow these instructions with any type: "string" field.

Make a basic React component

Start by making a new folder in your Sanity Studio v3 project called components and inside it, a new file called MyCustomStringInput.jsx (if you use TypeScript, make sure the file ending is .tsx):

~/sanity-studio-v3
.
├── README.md
├── components
│   └── MyCustomStringInput.jsx
├── node_modules
├── package-lock.json
├── package.json
├── sanity.cli.js
├── sanity.config.js
├── schemas
└── static

Open the MyCustomStringInput.jsx file and add the following code to it:

// /components/MyCustomStringInput.jsx

export const MyCustomStringInput = (props) => {
  return <div>Value: {props.value}</div>
}

This component will only render the value of a field as a text string. You will replace this code with the actual input later in this lesson. For now, you want to make sure that you import this component into your schema and field configuration and that it is rendering in the correct place.

Import your new component to the schema file, and add it to the field property components.input:

// /schemas/post.js
import { MyCustomStringInput } from '../components/MyCustomStringInput'
export default { name: 'post', title: 'Post', type: 'document', fields: [ { name: 'title', title: 'Title', type: 'string',
components: {
input: MyCustomStringInput
}
}, // ... the rest of the fields ] }

That is it! Make sure both files are saved and run the local development server with npm run . Open a post document to check if your component renders. If it’s new, you will only see “Value:”, and if had a Post document there from before, you will also see the contents of the title field being rendered.

You have now learned how to add a custom React component to your Sanity Studio v3 project and how to import it into the schema configuration.

Importing an input element

In theory, you can use any input component from the HTML specification supported by a modern browser or even import a React component published on npm. As long as the component has a way to call a function whenever it has changes made to it. In this lesson, you will import the TextInput component from Sanity UI. Sanity UI is the component library that is already used to make everything you see in the Studio. Using its components makes it easier to keep the editor experience accessible and consistent.

Start by importing the TextInput component from Sanity UI in your MyCustomStringInput.jsx file, and replace it with your current code. You don’t have to install @sanity/ui as a dependency since it comes with the Studio out of the box.

// /components/MyCustomStringInput.jsx
import {TextInput} from '@sanity/ui'
export const MyCustomStringInput = (props) => {
return <TextInput />
}

Save the file and make sure your development server is running. You should now see a string input field in your Post document. As it is now, it will do nothing when you type into it, and it will not show any content that you might have in the title field from before.

Adding necessary properties and making sure the field gets focus

Sanity Studio v3’s form has a lot of accessibility and behavior built into it. When we add custom components, it is important to ensure we are also accommodating this behavior. Fortunately, a custom input component receives properties (props) that help you with that. At least for basic inputs. In the code under, you can see what these properties are:

// /components/MyCustomStringInput.jsx
import {TextInput} from '@sanity/ui'

export const MyCustomStringInput = (props) => {
const {
elementProps: {
id,
onBlur,
onFocus,
placeholder,
readOnly,
ref,
// value
},
onChange,
schemaType,
validation,
value = ''
} = props
return <TextInput /> }

It can be useful to know what some of these different properties are responsible for:

  • ref is used by the Studio to automatically set focus to an input if it’s first in a form or a view. This is great for editor experience, and accessibility
  • onBlur brings in the Studio behavior when the input loses focus.
  • onChange is where you will add a handler function that is responsible for patching data to the content lake when the value of the input changes
  • onFocus brings in the Studio behavior for when an input gets focus
  • readOnly is the behavior for when the value is set to be read-only
  • schemaType contains different information that comes from the current schema type that this input is connected to. You will use this to add any placeholder that comes in the input configuration.
  • validation is a list of any validation issues related to this particular value. We will not use it in this lesson.

value is the current content of the input that’s stored in the Content Lake. This will update in real-time whenever someone makes changes to a particular field in a document. Notice that there is a value inside of elementProps that will always be converted to a string.

Gotcha

Difference between elementProps.value and props.value

props.value will always be either undefined or a value of the data type mandated by the schema.

elementProps.value will always be a string:

  • An empty string if the input has no value
  • A string with a numeric value if the input type is a number
  • Either "true" or "false" if the input type is a boolean (or empty string if the input has no value)

Passing elementProps

You can now pass and spread the elementProps to the input component. Many of these will “just work” on any input component from Sanity UI and on most HTML input elements.

// /components/MyCustomStringInput.jsx
import {TextInput} from '@sanity/ui'

export const MyCustomStringInput = (props) => {
const { elementProps } = props
return (
<TextInput {...elementProps} />
) }

What you have now is actually the same string input component that Sanity Studio v3 comes with out of the box. To make it a bit more interesting and… custom, you can add a reactive character counter below it.

Using Sanity UI to make it look nicer

First, import the Stack and Text components from Sanity UI. These will make sure that your component looks good. The Stack component wraps the whole input component and makes it possible to get consistent spacing (space={2}) between the elements and the Text component provides the correct padding and font for the custom text.

// /components/MyCustomStringInput.jsx
import {Stack, Text, TextInput} from '@sanity/ui'
export const MyCustomStringInput = (props) => { const { elementProps, value = '' } = props return (
<Stack space={2}>
<TextInput {...elementProps} />
<Text>Characters: {value.length}</Text>
</Stack>
) }

Excellent work so far! You have now implemented the necessary properties for making sure the input component behaves in an accessible way, gets the current value, and even added a bit of customization with the character count text below it.

You have almost everything you need in place for this custom input component to work. The only thing you miss is the mechanism for actually updating content in real-time to the Content Lake.

Patching content to the Content Lake

Sanity Studio and Content Lake are built to provide a real-time editing experience that allows multiple people (and machines) to simultaneously make changes to the same document. In order for this to work, you need to add a change handler to the input component that emits (read: sends) a patch with its new value to the Content Lake.

Fortunately, the Studio handles all the hard parts of that, and all you have to do is to call a function called onChange that is passed through a custom input component’s props.

The onChange function takes the return value of a patch function as its argument. In this lesson, you will use the set() and the unset() patch functions that generate the patch object that’s sent to the Content Lake.

Importing patch functions from sanity

Start by importing the set and unset functions from sanity, make a new function inside the component called handleChange, and add it to the TextInput's onChange prop:

// /components/MyCustomStringInput.jsx
import {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 = (event) => {/* more code to come */}
return ( <Stack space={2}> <TextInput {...elementProps}
onChange={handleChange}
value={value} /> <Text>Characters: {value.length}</Text> </Stack> ) }

Now you can add the final piece of logic, where you first extract the input element’s current value from the event that is passed to the handleChange function, and use it to update the Content Lake:

// /components/MyCustomStringInput.jsx
import {Stack, Text, TextInput} from '@sanity/ui'
import {set, unset} from 'sanity'

export const MyCustomStringInput = (props) => {
  const {elementProps, onChange, value = ''} = props
  
const handleChange = (event) => {
const nextValue = event.currentTarget.value
onChange(nextValue ? set(nextValue) : unset())
}
return ( <Stack space={2}> <TextInput {...elementProps} onChange={handleChange} value={value} /> <Text>Characters: {value.length}</Text> </Stack> ) }

Note that inside the onChange function, you have a ternary if statement that will remove the field (unset()) from the document if the input component is empty.

Adding performance optimization with useCallback

You should also consider the performance of this custom input component. Especially since you are using React and since you are adding it to a real-time environment where the state can change a lot. Without going too far into the technicalities, what you want to avoid is that React unnecessarily re-renders your component and unnecessarily re-declares variables and functions.

React comes with a hook called useCallback that lets you wrap a function inside of a component and declare which props should make the component redeclare your handler function. In this case, that’s only changes that are passed into the onChange prop.

Start by importing the useCallback hook from React and wrap the anonymous function in it, adding onChange to its dependency array:

// /components/MyCustomStringInput.jsx
import {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 space={2}> <TextInput {...elementProps} onChange={handleChange} value={value} /> <Text>Characters: {value.length}</Text> </Stack> ) }

And that’s it!

Next steps

Congratulations! You have now built your own custom input component for a string field. You have done so by importing a React component to the components.input property on a field configuration. You have used Sanity UI to build a custom user interface that will be consistent with how the Studio looks and works. Your string input component has additional functionality for counting how many characters it has. You have added a mechanism for updating content in real-time to the Content Lake and made sure that the input component is performant.

Have feedback?

Were you able to follow this lesson? Anything that was unclear or you wished we had explained better? Or was it awesome, and you want to let us know?

Head over to our GitHub Discussions and share your thoughts in the thread belonging to this lesson. Thanks for your time and attention!








Other guides by author