Build Your Own Fields
You can create your own fields for your website project, either for personal use or to share it privately or publicly with others.
To get started, check some of the existing fields in Headcode CMS — like TextField, TextareaField, or ImageField — and use them as inspiration for your own custom fields.
Custom fields allow you to extend Headcode CMS with your own input types, validations, and admin UI components.
Structure
Each field consists of two parts:
Field definition: defines the structure, type, and validation (e.g., text-field.ts)
Field component: defines how the field is rendered and edited inside the Headcode Admin (e.g., text-field-component.tsx)
Examples
import { lazy } from 'react'import { z } from 'zod'import type { FieldProps } from '@/lib/headcode/types'const DefaultTextField: FieldProps<string, unknown> = { label: 'Text Field', component: lazy(() => import('./text-field-component')), defaultValue: '', validator: z.string(),}export const TextField = (params: Partial<FieldProps<string, unknown>>) => ({ ...DefaultTextField, ...params,})import { Field, FieldDescription, FieldError, FieldLabel,} from '@/components/ui/field'import { Input } from '@/components/ui/input'import { useFieldContext } from './app-form'export default function TextFieldComponent({ label, description,}: { label: string description?: string | undefined}) { const field = useFieldContext<string>() const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid return ( <Field data-invalid={isInvalid}> <FieldLabel htmlFor={field.name}>{label}</FieldLabel> <Input id={field.name} name={field.name} value={field.state.value} onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)} aria-invalid={isInvalid} autoComplete="off" /> {description && <FieldDescription>{description}</FieldDescription>} {isInvalid && <FieldError errors={field.state.meta.errors} />} </Field> )}Note, that with const field = useFieldContext<string>() you get the TanStack field context.
The Link Field combines multiple fields into a new field type:
import { lazy, type ComponentType } from 'react'import { z } from 'zod'import type { FieldProps } from '@/lib/headcode/types'export type LinkValue = { title: string url: string openInNewWindow: boolean}const DefaultLinkField: FieldProps<LinkValue, unknown> = { label: 'Link Field', component: lazy(() => import('./link-field-component')) as ComponentType<{ label: string description?: string options?: unknown }>, defaultValue: { title: '', url: '', openInNewWindow: false, }, validator: z.object({ title: z.string(), url: z.string(), openInNewWindow: z.boolean().default(false), }),}export const LinkField = (params: Partial<FieldProps<LinkValue, unknown>>) => ({ ...DefaultLinkField, ...params,})import { Field, FieldDescription, FieldError, FieldLabel,} from '@/components/ui/field'import { Input } from '@/components/ui/input'import { Checkbox } from '@/components/ui/checkbox'import { useFieldContext } from './app-form'import type { LinkValue } from './link-field'export default function LinkFieldComponent({ label, description,}: { label: string description?: string | undefined}) { const field = useFieldContext<LinkValue>() const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid const value = field.state.value || { title: '', url: '', openInNewWindow: false, } const handleTitleChange = (title: string) => { field.handleChange({ ...value, title, }) } const handleUrlChange = (url: string) => { field.handleChange({ ...value, url, }) } const handleOpenInNewWindowChange = (openInNewWindow: boolean) => { field.handleChange({ ...value, openInNewWindow, }) } return ( <Field data-invalid={isInvalid}> {isInvalid && <FieldError errors={field.state.meta.errors} />} <div className="space-y-4"> <div className="flex flex-col gap-4 md:flex-row md:gap-4"> <div className="flex-1"> <FieldLabel htmlFor={`${field.name}-title`} className="text-sm"> {label} </FieldLabel> <Input id={`${field.name}-title`} name={`${field.name}.title`} value={value.title} onBlur={field.handleBlur} onChange={(e) => handleTitleChange(e.target.value)} aria-invalid={isInvalid} autoComplete="off" className="mt-1" /> </div> <div className="flex-1"> <FieldLabel htmlFor={`${field.name}-url`} className="text-sm"> URL </FieldLabel> <Input id={`${field.name}-url`} name={`${field.name}.url`} value={value.url} onBlur={field.handleBlur} onChange={(e) => handleUrlChange(e.target.value)} aria-invalid={isInvalid} autoComplete="off" className="mt-1" /> </div> </div> <div className="flex items-center gap-2"> <Checkbox id={`${field.name}-openInNewWindow`} name={`${field.name}.openInNewWindow`} checked={value.openInNewWindow} onCheckedChange={(checked) => handleOpenInNewWindowChange(checked === true) } onBlur={field.handleBlur} aria-invalid={isInvalid} /> <FieldLabel htmlFor={`${field.name}-openInNewWindow`} className="cursor-pointer text-sm font-normal" > Open in new window </FieldLabel> </div> </div> {description && <FieldDescription>{description}</FieldDescription>} </Field> )}