Разработка кастомных компонентов ввода (Input Components) Sanity
Sanity позволяет заменить стандартный UI поля на кастомный React компонент. Используется для сложных типов ввода: карты, тегов с автодополнением, цветовых палитр, нестандартных редакторов.
Базовый кастомный Input
// components/studio/ColorPickerInput.tsx
import { useCallback } from 'react'
import { set, unset } from 'sanity'
import type { StringInputProps } from 'sanity'
const PRESET_COLORS = ['#FF5733', '#33FF57', '#3357FF', '#FF33A8', '#FFAA00', '#00AAFF', '#9B59B6', '#1ABC9C']
export function ColorPickerInput(props: StringInputProps) {
const { value, onChange, readOnly, elementProps } = props
const handleSelect = useCallback(
(color: string) => {
onChange(color ? set(color) : unset())
},
[onChange]
)
return (
<div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 8 }}>
{PRESET_COLORS.map(color => (
<div
key={color}
title={color}
onClick={() => !readOnly && handleSelect(color)}
style={{
width: 28,
height: 28,
borderRadius: '50%',
background: color,
cursor: readOnly ? 'default' : 'pointer',
border: value === color ? '3px solid var(--card-focus-ring-color)' : '2px solid transparent',
outline: value === color ? '2px solid white' : 'none',
boxSizing: 'border-box',
}}
/>
))}
</div>
<input
{...elementProps}
type="text"
value={value || ''}
onChange={e => handleSelect(e.target.value)}
placeholder="#000000 или rgb(0,0,0)"
style={{ width: '100%' }}
/>
{value && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 }}>
<div style={{ width: 20, height: 20, background: value, borderRadius: 3 }} />
<span style={{ fontSize: 12 }}>{value}</span>
</div>
)}
</div>
)
}
// Подключение в схеме
defineField({
name: 'brandColor',
type: 'string',
components: {
input: ColorPickerInput,
},
})
Tag Input с автодополнением
// components/studio/TagInput.tsx
import { useState, useCallback } from 'react'
import { set, insert, remove } from 'sanity'
import type { ArrayInputProps, StringInputProps } from 'sanity'
const SUGGESTED_TAGS = ['TypeScript', 'React', 'Next.js', 'DevOps', 'Python', 'Docker']
export function TagInput(props: ArrayInputProps) {
const { value = [], onChange, readOnly } = props
const [input, setInput] = useState('')
const addTag = useCallback(
(tag: string) => {
const trimmed = tag.trim()
if (!trimmed || (value as string[]).includes(trimmed)) return
onChange(insert([trimmed], 'after', [-1]))
setInput('')
},
[value, onChange]
)
const removeTag = useCallback(
(index: number) => {
onChange(remove([{ _key: `tag-${index}` }]))
},
[onChange]
)
const suggestions = SUGGESTED_TAGS.filter(
t => t.toLowerCase().includes(input.toLowerCase()) && !(value as string[]).includes(t)
)
return (
<div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 8 }}>
{(value as string[]).map((tag, i) => (
<span
key={i}
style={{
padding: '2px 10px',
background: 'var(--card-border-color)',
borderRadius: 12,
fontSize: 13,
display: 'flex',
alignItems: 'center',
gap: 4,
}}
>
{tag}
{!readOnly && (
<button onClick={() => removeTag(i)} style={{ background: 'none', border: 'none', cursor: 'pointer' }}>
×
</button>
)}
</span>
))}
</div>
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); addTag(input) } }}
placeholder="Добавить тег..."
disabled={readOnly}
/>
{suggestions.length > 0 && input && (
<div style={{ border: '1px solid var(--card-border-color)', borderRadius: 4 }}>
{suggestions.map(tag => (
<div key={tag} onClick={() => addTag(tag)} style={{ padding: '6px 12px', cursor: 'pointer' }}>
{tag}
</div>
))}
</div>
)}
</div>
)
}
Slug с preview URL
// components/studio/SlugInput.tsx
import { useCallback } from 'react'
import { SlugInput as DefaultSlugInput } from 'sanity'
import type { SlugInputProps } from 'sanity'
export function SlugWithPreview(props: SlugInputProps) {
const { value } = props
const slug = value?.current
return (
<div>
<DefaultSlugInput {...props} />
{slug && (
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--card-muted-fg-color)' }}>
URL:{' '}
<a
href={`${process.env.SANITY_STUDIO_PREVIEW_URL}/posts/${slug}`}
target="_blank"
rel="noreferrer"
>
/posts/{slug}
</a>
</div>
)}
</div>
)
}
Поле с внешними данными
// components/studio/RegionSelect.tsx
import { useState, useEffect } from 'react'
import { set } from 'sanity'
import type { StringInputProps } from 'sanity'
export function RegionSelect(props: StringInputProps) {
const { value, onChange } = props
const [regions, setRegions] = useState<{ id: string; name: string }[]>([])
useEffect(() => {
fetch('/api/regions').then(r => r.json()).then(setRegions)
}, [])
return (
<select value={value || ''} onChange={e => onChange(set(e.target.value))}>
<option value="">— Выберите регион —</option>
{regions.map(r => (
<option key={r.id} value={r.id}>{r.name}</option>
))}
</select>
)
}
Сроки
Разработка 3–5 кастомных input компонентов — 2–3 дня.







