Create a visual string selector field input
Go beyond a plain radio list of inputs by giving authors more contextually useful buttons to select values from.
This developer guide was contributed by Simeon Griggs (Principal Educator).
Go beyond a plain radio list of inputs by giving authors more contextually useful buttons to select values from.
What you need to know
This guide assumes that you know how to set up and configure a Sanity Studio and have basic knowledge about defining a schema with document and field types. Basic knowledge of React and TypeScript is also useful, although you should be able to copy-paste the example code to get a runnable result.
The examples require sanity 6.4.0 or later, which installs @sanity/ui 3.3.0 or later and @sanity/icons 5.x. Older versions don’t provide the Grid component’s gridTemplateColumns prop, the Stack component’s gap prop, or per-icon imports such as @sanity/icons/User.
Custom form components by example
One of Sanity Studio’s most powerful features is custom drop-in replacements for form fields. This guide is one in a series of code examples.
You can get more familiar with the Form Components API in the documentation.
- Create a “coupon generator” string field input
- Create a visual string selector field input
- Create a survey rating number field input
- Create a time duration object field
- Create an array input field with selectable templates
- Create interactive array items for featured elements
- Create richer array item previews
- Create a document form progress component
What you’ll create
A string field with larger buttons to select a value packed with more relevant information.
Create the schema types
In this guide, you’ll be creating a document type for a product feature. Each feature is only available on a certain plan and above. Plans are selected from a list of predefined strings.
Create the following schema type files and ensure they’re loaded into the schema property in sanity.config.ts.
First, create a custom string type called plan. Creating a new schema type for this string type allows more flexible reuse throughout your studio. For example, several document types can use this plan type and its custom input, each with its own options. By importing this schema type to the Studio schema, you can refer to this type with its value for name, in other words type: 'plan'.
import {defineType} from 'sanity'
// You'll extend these and import them in the custom input component later
export const PLANS = [
{title: 'Free', value: 'free'},
{title: 'Premium', value: 'premium'},
{title: 'Enterprise', value: 'enterprise'},
]
export const planType = defineType({
name: 'plan',
title: 'Plan',
type: 'string',
options: {
list: PLANS.map(({title, value}) => ({title, value})),
layout: 'radio',
},
})Now, add a feature document type:
import {defineField, defineType} from 'sanity'
export const featureType = defineType({
name: 'feature',
title: 'Feature',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'plan',
type: 'plan',
description: 'Minimum plan required to access this feature',
}),
],
})Register both schema types in the schema property of sanity.config.ts:
import {defineConfig} from 'sanity'
import {planType} from './schema/plan/planType'
import {featureType} from './schema/featureType'
export default defineConfig({
// ...all other settings
schema: {
types: [planType, featureType],
},
})Create a new feature type document. The form shows both string fields:
Create an input component
Let’s say you want to add more information to these plans with an icon and a description. The neat solution would be to have a document type for plans and bring the selection from them, but for the sake of simplicity, you’ll be hard coding this information by extending the PLANS array with properties and values for icon and description. The out-of-box radio list doesn’t support these properties, but you’ll build a UI for them in the custom input component:
import {defineType} from 'sanity'
import {UserIcon} from '@sanity/icons/User'
import {UsersIcon} from '@sanity/icons/Users'
import {EarthGlobeIcon} from '@sanity/icons/EarthGlobe'
export const PLANS = [
{title: 'Free', value: 'free', description: 'For personal use', icon: UserIcon},
{title: 'Premium', value: 'premium', description: 'For small teams', icon: UsersIcon},
{title: 'Enterprise', value: 'enterprise', description: 'For large teams', icon: EarthGlobeIcon},
]
export const planType = defineType({
name: 'plan',
title: 'Plan',
type: 'string',
options: {
list: PLANS.map(({title, value}) => ({title, value})),
layout: 'radio',
},
})Create a new input component using the following code:
import {StringInputProps, set} from 'sanity'
import {Stack, Button, Grid, Label, Text} from '@sanity/ui'
import {createElement} from 'react'
import {PLANS} from './planType'
export function PlanInput(props: StringInputProps) {
const {value, onChange} = props
return (
<Grid gridTemplateColumns={PLANS.length} gap={3}>
{PLANS.map((plan) => (
<Button
key={plan.value}
value={plan.value}
mode={value === plan.value ? `default` : `ghost`}
tone={value === plan.value ? `primary` : `default`}
>
<Stack gap={3} padding={2}>
<Text size={4} align="right">
{createElement(plan.icon)}
</Text>
<Label>{plan.title}</Label>
<Text>{plan.description}</Text>
</Stack>
</Button>
))}
</Grid>
)
}The component compares the field’s current value with each plan in PLANS to highlight the selected button.
To see this component in the Studio, you’ll need to add it to the plan schema type:
import {defineType} from 'sanity'
import {PlanInput} from './PlanInput'
export const planType = defineType({
name: 'plan',
// ...all other settings
components: {input: PlanInput},
})Now when editing the document you’re shown a beautiful set of buttons with much more details and context for your authors.
You can click these buttons but they won’t write anything to the document, yet!
Handle changes and patch data
Custom inputs contain helpful functions and details in their props. For this input, you’ll only need one: onChange.
This function wraps any patch (such as setting or unsetting the value of a field) and ensures the rest of the Studio stays up to date with changes.
When working with forms in React, you’re often recommended to store values in a component’s state. This is an anti-pattern when working with Sanity Studio input components. Writing content to state is only reflected in the browser of the person using the input. By using Sanity’s real-time APIs you allow content creators to collaborate and avoid overwriting each other’s changes by always syncing directly to the Content Lake.
Update your PlanInput component to use the following code:
import {StringInputProps, set} from 'sanity'
import {Stack, Button, Grid, Label, Text} from '@sanity/ui'
import {useCallback, createElement} from 'react'
import {PLANS} from './planType'
export function PlanInput(props: StringInputProps) {
const {value, onChange} = props
const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
const nextValue = event.currentTarget.value
onChange(set(nextValue))
},
[onChange]
)
return (
<Grid gridTemplateColumns={PLANS.length} gap={3}>
{PLANS.map((plan) => (
<Button
key={plan.value}
value={plan.value}
mode={value === plan.value ? `default` : `ghost`}
tone={value === plan.value ? `primary` : `default`}
onClick={handleClick}
>
<Stack gap={3} padding={3}>
<Text size={4} align="right">
{createElement(plan.icon)}
</Text>
<Label>{plan.title}</Label>
<Text>{plan.description}</Text>
</Stack>
</Button>
))}
</Grid>
)
}- Notice how
onChangeis destructured from the component’s props. - It is then called inside the
handleClickfunction with theset()function, to update the field’s value in the Content Lake. This means the new value will be instantly validated in the document and updated in the browser of any other authors currently viewing the same document. - Both the
modeandtoneof the button are updated to highlight which value is currently selected.
Now you have a fully functional visual string selector field with richly detailed buttons.
Next steps
Some ideas to extend this custom input include:
- A similar component with richly detailed buttons but for selecting a
referenceinstead of astring. - Import
unsetfromsanityand add an extraButtonto remove the value from the document.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?