Skip to content

Field types

Trokky registers 23 field types. Every one of them is a plugin: a type string, an editor component, a validator that runs in the browser, and a preview for list views. Your schema names the type string; the registry supplies the rest.

fields: {
title: { type: 'string', title: 'Title', required: true },
}

Two things apply to all of them, and both are easier to learn here than to discover later.

title is what the editor sees. It is not optional in practice — a field without one shows up unlabelled.

options and validation are read in the browser. The Studio’s field plugin reads them. The server’s validator knows nine types (string, number, boolean, date, array, object, reference, media, slug) and, for those, checks the shape of the value and whether a required field is present. It does not read maxLength, min, minItems, pattern or any other per-type rule, and for the fourteen types outside that list it accepts whatever you send. Schemas explains the split and what it means for anything writing to the API directly.

typeStoresEditorFrontend receives
stringstringSingle-line input, or a dropdown with options.liststring
textstringAuto-resizing textareastring
emailstringInput with email validationstring
urlstringInput with protocol validationstring
passwordstringMasked input, optional strength meter and generatorstring
slugstringInput with a generate-from-source buttonstring
numbernumberNumeric input, or a slidernumber
booleanbooleanCheckbox, toggle, radio or buttonboolean
dateISO date stringCalendar, optionally with a time pickerstring
richtextHTML string by defaultWYSIWYG editorstring, or a ProseMirror document
portable{ blocks, metadata }Block editorPortableTextContent
media{ _type: 'media', asset: { _ref, _type } }Upload and media browserAsset reference — resolve it to a URL
imageSame as mediaMedia browser filtered to imagesSame as media
videoSame as mediaMedia browser filtered to videoSame as media
audioSame as mediaMedia browser filtered to audioSame as media
documentSame as mediaMedia browser filtered to documentsSame as media
reference{ _ref, _type }, or an array of themSearchable document pickerThe reference, or the whole document with expand
arrayunknown[]Repeatable list, grid, tag input or checkbox setArray of the item type
objectRecord<string, unknown>Nested form, collapsible or tabbedObject with your field names
colorstringColour picker with swatchesstring
icon{ library, name, style?, svg? }Searchable icon gridThe icon descriptor
geoCoordinate{ lat, lng, alt?, accuracy? }Map plus manual entryThe coordinate object
infoNothingA callout box in the formNothing — it is never stored

There is no datetime type. Time is an option on date.

string is the default single-line field.

title: {
type: 'string',
title: 'Title',
required: true,
validation: { maxLength: 100 },
options: { placeholder: 'Post title' },
},

validation reads minLength, maxLength, pattern, email, url. options reads inputType (text, email, url, tel, password), multiline, rows, autoComplete, spellCheck, transform (lowercase, uppercase, capitalize), size, and list.

list turns the field into a dropdown, and it is the reason there is no separate “select” type:

status: {
type: 'string',
title: 'Status',
options: {
list: [
{ title: 'Draft', value: 'draft' },
{ title: 'In review', value: 'review' },
],
},
},

A plain array of strings works too. Either way the stored value is the string, and nothing stops the API from storing a value outside the list.

text is the multi-line version. validation reads minLength, maxLength, wordCount, lineCount. options reads rows, minRows, maxRows, autoResize, spellCheck, wrap, size.

email is string with email validation forced on, and url validates the protocol — validation reads protocols (default ['http:', 'https:']) and requireHttps, options reads placeholder and openInNewTab.

password renders a masked input. validation reads minLength (default 8), maxLength (default 128), requireUppercase, requireLowercase, requireNumbers, requireSpecialChars, pattern. options reads showStrength, allowToggle, disableAutocomplete, showGenerator and a generator block (length, includeUppercase, includeLowercase, includeNumbers, includeSpecialChars, customChars, excludeSimilar).

The value a password field holds is stored in the document as an ordinary string, and any client that can read the document can read it. This field is for content that happens to be a secret-shaped string, not for credentials. Trokky’s own user accounts live somewhere else entirely — see Users and authentication.

slug stores a plain string. It does not store a wrapper object, so post.slug is the slug.

slug: {
type: 'slug',
title: 'URL',
source: 'title',
autoGenerate: true,
unique: true,
allowSlashes: true,
prefix: 'blog/',
},

Slug options sit on the field itself, not under options — this is the one type that works that way, because the server reads them too. The keys: source (a field name or a list of them, tried in order), autoGenerate (default true), unique (default true), maxLength (default 200), minLength (default 1), allowEmpty (default false), readOnly, preserveCase (default false), allowedChars, allowSlashes (default true), prefix, suffix.

Generation happens on the server when a document is created or updated without a slug. The input is normalised: unicode decomposed, accents dropped, lowercased unless preserveCase, spaces to hyphens, runs of hyphens collapsed, leading and trailing hyphens removed. Été à Lomé becomes ete-a-lome.

unique appends -2, -3 and so on until the slug is free within the collection. That means a second document titled the same as the first gets my-post-2 — the slug is not the title, and two documents can drift apart in a way that is invisible in the Studio.

allowSlashes is on by default, so blog/2026/my-post is a valid slug. If you are routing on the slug, decide whether you want that before an editor discovers it.

Every schema with a string field gets a slug field injected if it does not declare one. See Schemas.

number stores a JavaScript number. validation reads min, max, step, precision, integerOnly, positiveOnly, nonNegativeOnly. options reads format (decimal, currency, percentage, scientific), currency, locale, showThousandSeparator, showSpinButtons, prefix, suffix, autoFormat, displayMode (input or slider), showValue. Formatting is display only — the stored value is the raw number.

boolean stores true or false. validation reads mustBeTrue and mustBeFalse. options reads style (checkbox, toggle, radio, button), label, trueText, falseText, size, color, labelPosition, disabledStyle.

A boolean with no default and no editor interaction is absent from the document, not false. Read it as post.featured === true rather than trusting the type.

date stores a date string. validation reads min, max, disablePast, disableFuture, disableWeekends, disabledDates, allowedDates. options reads dateFormat, displayFormat, includeTime, timeFormat (12h or 24h), minTime, maxTime, showCalendar, calendarStartDay, placeholder, clearable, autoFocus.

default: 'now' is a recognised sentinel and resolves to the current time when the Studio creates a document.

publishedAt: {
type: 'date',
title: 'Published at',
options: { includeTime: true },
default: 'now',
},

richtext is the WYSIWYG field. By default it stores an HTML string, which is why the usual frontend rendering is a raw HTML insertion:

<div set:html={post.body} />

options.outputFormat changes what gets stored:

  • 'html' — an HTML string. The default.
  • 'prosemirror' — a ProseMirror/TipTap JSON document, { type: 'doc', content: [...] }.
  • 'markdown' — a Markdown string.

Changing outputFormat on a schema that already has content does not convert the stored documents. Existing documents keep the old format and your rendering code has to handle both, so pick the format before editors start writing.

Other options: toolbar (an array of button names), headingLevels, theme, spellCheck, showStats, showCharacterCount, showWordCount, showReadTime, enableMediaUpload, enableFullscreen, minHeight, maxHeight, editorClasses, placeholder, and pasteSecurity.

validation reads minLength, maxLength, minWords, maxWords, requiredElements, prohibitedElements. The default limits are 10,000 characters and 2,000 words, enforced in the editor only.

pasteSecurity governs what survives a paste from Word or a web page:

options: {
pasteSecurity: {
mode: 'safe', // 'strict' | 'safe' | 'permissive'
linkPolicy: 'sanitize', // 'strip' | 'sanitize' | 'validate'
imagePolicy: 'strip', // 'strip' | 'proxy' | 'allow'
maxPasteLength: 10000,
},
},

The default is safe with images stripped, so pasting an article from another site brings the text and drops the pictures. Editors notice this and report it as a bug; it is the configured behaviour.

portable is the block-structured alternative. It stores { blocks, metadata }, where each block carries a _key, a _type, a style, children spans with marks, and markDefs for links and references. Use it when the frontend needs to render blocks itself rather than inject HTML. options reads enabledBlockTypes, enabledMarks, enabledStyles, defaultBlockType, maxNestingDepth, spellCheck, theme, showBlockCount, showCharacterCount, showWordCount, enableFullscreen, collapsible, pasteSecurity. validation reads minBlocks, maxBlocks, minLength, maxLength, allowedBlockTypes, requiredBlockTypes, allowedMarks, allowedStyles.

The two are unrelated storage formats. Nothing converts between them.

media stores a reference to an uploaded asset, not the file and not a URL:

{
"_type": "media",
"asset": { "_ref": "media-abc123", "_type": "mediaAsset" },
"alt": "Harbour at dawn",
"caption": "Lomé, 2026",
"title": "Harbour",
"variant": "hero"
}

alt, caption, title and variant are per-use: the same asset referenced from two documents can carry different alt text in each. Asset-level metadata — dimensions, size, content type, generated variants — lives with the asset, not here.

Your frontend gets that object and has to turn it into a URL. The client does it:

const url = client.imageUrl(post.coverImage)
.width(800)
.format('webp')
.url()

.height(), .quality(), .fit(), .blur(), .variant() and .auto() are also available. Images and media covers the pipeline, including the case where transforms are not applied at all.

validation reads allowedTypes (MIME types), allowedExtensions, restrictToMediaType, maxFileSize (default 10 MB), minFileSize, maxWidth, maxHeight, minWidth, minHeight, aspectRatio, aspectRatioTolerance, maxDuration, minDuration, blockDangerousExtensions (default true), requireVirusScan.

options reads enableUpload, enableBrowse, enableDragDrop, showPreview, previewSize, showMetadata, mediaTypeFilter, showVariantSelector, uploadPath, requireAlt, requireCaption, requireAssetTitle, requireAssetDescription.

coverImage: {
type: 'media',
title: 'Cover image',
validation: {
restrictToMediaType: 'image',
maxFileSize: 5 * 1024 * 1024,
},
options: { requireAlt: true },
},

These four are the same field with the type filter and the sensible extension list already applied. They store exactly the same value as media_type on the stored object stays 'media', so your rendering code does not branch on which one you declared.

poster: { type: 'image', title: 'Poster' },

is shorthand for a media field restricted to images. Each adds a few display options of its own: image takes showDimensions, showFileSize, cropEnabled, resizeEnabled, quality; video takes showThumbnail, showDuration, autoplay, muted, controls, and validation.maxResolution, minResolution, allowedCodecs; audio takes showWaveform, showDuration, autoplay; document takes showPageCount, showFileSize, allowPreview, allowDownload, and validation.maxPages, minPages, requiresPassword.

Use them for the clearer intent in the schema. Use media when a field should accept more than one kind of file.

reference points at another document. It stores { _ref, _type } — the target’s id and its collection — and nothing else.

category: {
type: 'reference',
title: 'Category',
to: 'category',
},

Omit to and the field accepts any document type; options.includeTypes and options.excludeTypes narrow that down. validation.multiple: true makes it an array of references.

By default your frontend receives the reference, not the document. Getting the document back is the expand query parameter, and it is not free. References is the page for both.

validation reads multiple, minReferences, maxReferences. options reads showPreview, allowCreate, displayField, previewFields, enableSearch, searchPlaceholder, maxSearchResults, groupByType, pickerLayout, sortable, showCount, filter, includeTypes, excludeTypes.

array repeats one item definition, given by of:

tags: {
type: 'array',
title: 'Tags',
of: { name: 'tag', type: 'string', title: 'Tag' },
options: { layout: 'tags' },
},

of is a single item definition, not a list of alternatives. An array holds one kind of thing. To hold a mixture, make the item an object with a discriminating field and branch on it when rendering.

options.layout changes the editor substantially, and the layouts are not interchangeable:

  • list — the default. A vertical, reorderable list with each item’s own editor.
  • grid — the same, in columns. gridColumns sets breakpoints, gridMaxWidth caps the width.
  • tags — a tag input for arrays of strings. tagField takes placeholder, allowCustom, suggestions, confirmDelete.
  • select — pick from a fixed set. selectField.options supplies the values, displayAs chooses checkboxes, pills or dropdown.
  • inline — items laid out in a row.

Other options: sortable (default true), insertAppend, collapsed, showCount, addButtonText, disableAdd, disableRemove, preview (showWhenCollapsed, maxPreviewItems, template).

validation reads minItems, maxItems (default 100), unique, uniqueBy. All four are editor rules.

An array of objects:

links: {
type: 'array',
title: 'Links',
of: {
name: 'link',
type: 'object',
title: 'Link',
fields: [
{ name: 'label', type: 'string', title: 'Label', required: true },
{ name: 'href', type: 'url', title: 'URL', required: true },
],
},
},

Two things about arrays that catch people out. Sending an empty array is how you clear one — a field left out of an update means “unchanged”, per Traps. And the required flag on an item’s fields is checked by the editor, not by the API, so an array written through the API can contain items missing their required properties.

object nests a group of fields under one key. The value is a plain object with your field names:

seo: {
type: 'object',
title: 'SEO',
fields: {
metaTitle: { type: 'string', title: 'Meta title' },
metaDescription: { type: 'text', title: 'Meta description' },
noIndex: { type: 'boolean', title: 'Hide from search engines' },
},
options: { layout: 'collapsible', collapsed: true },
},

Your frontend reads post.seo?.metaTitle. The whole object is absent if nobody filled it in, so the optional chain is not decoration.

options.layout takes inline, collapsible (the default), modal, card, section, sections, columns or tabs. sections and tabs need matching configuration listing which fields go where:

options: {
layout: 'tabs',
tabs: [
{ title: 'Content', fields: ['metaTitle', 'metaDescription'] },
{ title: 'Indexing', fields: ['noIndex'] },
],
},

A field missing from every tab is not rendered. That is the usual way an object field appears to lose a property after someone adds a tab.

Other options: collapsible, collapsed, showFieldCount, titleTemplate, columns, spacing, showDescriptions, fieldOrder, preview, modal, animations. validation reads requiredFields, additionalProperties, minProperties, maxProperties.

Objects nest, and so do arrays of objects. The server validates nested fields with the same nine-type rule as the top level.

color stores a string. options reads format (hex, rgb, rgba, hsl, hslahex by default), enableAlpha, swatches (a Material palette by default), defaultValue, showFormatSwitcher, showInput, enableEyedropper. validation reads allowedColors and forbiddenColors. The field’s own format check accepts hex only, so if you set format: 'rgb' verify the value where you read it.

icon stores { library, name, style?, svg? }. library is one of fontawesome, heroicons, lucide, material, custom. options reads library, libraries, style, heroiconsStyle, categories, allowSearch, showPreview, columns, pageSize, showRecent, customIcons. validation reads allowedLibraries and allowedCategories. What Trokky stores is the icon’s identity, not the icon — rendering it is your frontend’s job, with whichever icon package you already ship.

geoCoordinate stores { lat, lng, alt?, accuracy? }. options reads mapProvider (openstreetmap, google, mapbox), defaultZoom, enableGeolocation, showAltitude, showAccuracy, inputMode (map, manual, both), mapHeight, defaultCenter. validation reads bounds, requireAltitude, maxAccuracy.

info stores nothing. It renders a callout in the editing form and never appears in the document:

publishingNote: {
type: 'info',
title: 'Before publishing',
content: 'Check the cover image has alt text.',
options: { variant: 'warning' },
},

content is required and is the message. options reads variant (info, warning, tip, success, error), markdown (on by default), collapsible, defaultCollapsed, icon, className. It will still appear in your generated types as a property of the document interface, and it will always be undefined.