🎤 Builder Talk: The Story Behind Lady Gaga’s Digital Experience – Register now

Text Input with Presets

By Mitchell Christ

Want to add some preset buttons/chips below your text input field? Look no further!

TextInputWithPresets.tsx

'use client'

import { useCallback, type FormEvent } from 'react'
import { Badge, Card, Flex, Stack, Text, TextInput } from '@sanity/ui'
import type { StringInputProps, StringSchemaType } from 'sanity'

export type Preset =
  | string
  | {
      label: string
      value: string
    }

export function getPreset(
  preset: Preset,
  property: 'label' | 'value' = 'value',
) {
  return typeof preset === 'string' ? preset : preset[property]
}

export default function TextInputWithPresets({
  elementProps,
  prefix,
  suffix,
  presets,
}: {
  prefix?: string
  suffix?: string
  presets?: Preset[]
} & StringInputProps<StringSchemaType>) {
  const handleChange = useCallback(
    (value: string) => {
      elementProps.onChange({
        currentTarget: { value },
      } as FormEvent<HTMLInputElement>)
    },
    [elementProps.onChange],
  )

  return (
    <Stack space={2}>
      <Flex align="center" gap={1}>
        {prefix && (
          <Text size={1} muted>
            {prefix}
          </Text>
        )}

        <Card flex={1}>
          <TextInput
            {...elementProps}
            onChange={(e) => handleChange(e.currentTarget.value)}
          />
        </Card>

        {suffix && (
          <Text size={1} muted>
            {suffix}
          </Text>
        )}
      </Flex>

      {presets && (
        <Flex gap={1} paddingLeft={prefix ? prefix.length : undefined}>
          {presets?.map((preset) => {
            const presetValue = getPreset(preset)
            const label = getPreset(preset, 'label')

            return (
              <Badge
                style={{ cursor: 'pointer' }}
                padding={2}
                tone={
                  presetValue === elementProps.value ? 'primary' : 'default'
                }
                onClick={() => handleChange(presetValue)}
                key={presetValue}
              >
                {label}
              </Badge>
            )
          })}
        </Flex>
      )}
    </Stack>
  )
}

MySchema.tsx

import TextInputWithPresets, {
  getPreset,
  type Preset,
} from '@/sanity/ui/TextInputWithPresets'

const presets: Preset[] = [
  { label: 'Tablet and below', value: '(width < 48rem)' },
  { label: 'Mobile only', value: '(width < 24rem)' },
  { label: 'Dark mode', value: '(prefers-color-scheme: dark)' },
]

export default defineType({
  name: 'img',
  title: 'Image',
  type: 'object',
  fields: [
    defineField({
      name: 'image',
      type: 'image',
    }),
    defineField({
      name: 'media',
      title: 'Media query',
      type: 'string',
      placeholder: `e.g. ${presets.map((p) => getPreset(p)).join(', ')}`,
      initialValue: getPreset(presets[0]),
      components: {
        input: (props) => (
          <TextInputWithPresets
            prefix="@media"
            presets={presets}
            {...props}
          />
        ),
      },
    }),
  ],
})

😒 Want more to your basic text input field?

🥂 Want to add some presets as one-click buttons just below to save a couple seconds, but still have the ability to type custom values?

✨ This single file snippet (and easy implementation) adds some additional UX improvements.

👀 Here are some examples:

***

This custom input component is included out-of-the-box in SanityPress, a fully customizable Next.js + Sanity.io + Tailwind 4 starter template. Go check that out! 🖤

Contributor

Other schemas by author