Skip to content

Field Kit

EmDash’s json field type stores arbitrary structured data, but the default editor is a single-line text input where you have to type raw JSON by hand. Field Kit is a first-party plugin that ships four composable widgets for json fields, configured entirely through seed options — no React required from site builders.

Terminal window
npm i @emdash-cms/plugin-field-kit

Register the plugin in astro.config.mjs:

import { defineConfig } from "astro/config";
import emdash from "emdash";
import { fieldKitPlugin } from "@emdash-cms/plugin-field-kit";
export default defineConfig({
integrations: [
emdash({
plugins: [fieldKitPlugin()],
}),
],
});

Then attach a widget to any json field by setting widget to field-kit:<name>:

{
"slug": "ingredients",
"type": "json",
"widget": "field-kit:list",
"options": { "fields": [...] }
}
WidgetUse forStored value
object-formInline form for flat JSON objects{ key: value, ... }
listOrdered array editor with add / remove / reorder[{ ... }, ...]
gridRows × columns matrix{ rowKey: { colKey: value } }
tagsFree-form chip/tag input["tag1", "tag2"]

If a widget is missing its required options (e.g. fields for object-form/list, or rows/columns for grid), the editor renders an inline “Widget misconfigured” warning instead of a broken input — useful while iterating on seed schemas.

Renders a group of typed sub-fields that store as a single JSON object. Good for fixed-shape structured data like nutrition facts or contact info.

{
"slug": "nutrition",
"type": "json",
"widget": "field-kit:object-form",
"options": {
"collapsed": false,
"fields": [
{ "key": "calories", "label": "Calories", "type": "number", "suffix": "kcal" },
{ "key": "protein", "label": "Protein", "type": "number", "suffix": "g" },
{ "key": "fat", "label": "Fat", "type": "number", "suffix": "g" },
{ "key": "carbs", "label": "Carbs", "type": "number", "suffix": "g" }
]
}
}

Stored value: { "calories": 250, "protein": 12.5, "fat": 8, "carbs": 30 }.

OptionTypeDefaultDescription
fieldsSubFieldDef[](required)Sub-field definitions — see Sub-fields.
collapsedbooleanfalseRender the group collapsed by default.
helpTextstringHelp text shown below the widget.

An ordered array editor with add, remove, and reorder controls. Each row is a JSON object whose shape is defined by fields. The row header shows a summary rendered from a Mustache-style template.

{
"slug": "ingredients",
"type": "json",
"widget": "field-kit:list",
"options": {
"itemLabel": "Ingredient",
"min": 1,
"max": 50,
"sortable": true,
"summary": "{{name}} — {{amount}}",
"fields": [
{ "key": "name", "label": "Name", "type": "text", "required": true },
{ "key": "amount", "label": "Amount", "type": "text" },
{ "key": "optional", "label": "Optional", "type": "boolean" }
]
}
}

Stored value:

[
{ "name": "Flour", "amount": "500g", "optional": false },
{ "name": "Butter", "amount": "200g", "optional": false }
]
OptionTypeDefaultDescription
fieldsSubFieldDef[](required)Sub-field definitions for each row.
itemLabelstring"Item"Singular label for a row (used in the “Add” button and fallback row titles).
minnumberMinimum number of items. Below this, the remove button hides.
maxnumberMaximum number of items. At this count, the add button hides.
sortablebooleantrueShow up/down reorder buttons.
summarystringMustache template rendered as the collapsed-row title. See Summary templates.
helpTextstringHelp text shown below the widget.

A two-dimensional matrix of rows × columns. Each cell can be a toggle, text input, number input, or select. Useful for matrices like seasonal availability, price tables, or feature comparisons.

{
"slug": "availability",
"type": "json",
"widget": "field-kit:grid",
"options": {
"cell": "toggle",
"rows": [
{ "key": "berries", "label": "Berries" },
{ "key": "stoneFruit", "label": "Stone fruit" },
{ "key": "citrus", "label": "Citrus" }
],
"columns": [
{ "key": "spring", "label": "Spring" },
{ "key": "summer", "label": "Summer" },
{ "key": "autumn", "label": "Autumn" },
{ "key": "winter", "label": "Winter" }
]
}
}

Stored value:

{
"berries": { "spring": false, "summer": true, "autumn": false, "winter": false },
"stoneFruit": { "spring": false, "summer": true, "autumn": true, "winter": false },
"citrus": { "spring": false, "summer": false, "autumn": true, "winter": true }
}
OptionTypeDefaultDescription
rowsGridAxisDef[](required)Row definitions: { key, label, image? }.
columnsGridAxisDef[](required)Column definitions: { key, label, image? }.
cell"toggle" | "text" | "number" | "select""toggle"Cell input type, applied uniformly to every cell.
cellOptionsstring[] | Array<{ label, value }>[]Required when cell is "select".
helpTextstringHelp text shown below the widget.

A chip-style input for arrays of strings. Supports a fixed suggestions list, free-form custom values (toggleable), case transforms, and an optional max.

{
"slug": "keywords",
"type": "json",
"widget": "field-kit:tags",
"options": {
"placeholder": "Add a keyword…",
"max": 10,
"transform": "lowercase",
"allowCustom": true,
"suggestions": ["vegan", "vegetarian", "gluten-free", "dairy-free", "nut-free"]
}
}

Stored value: ["vegan", "gluten-free"].

Press Enter or , to commit a tag. Backspace on an empty input removes the last tag. Duplicate tags are silently ignored.

OptionTypeDefaultDescription
placeholderstring"Add..."Input placeholder shown when no tags are present.
maxnumberMaximum number of tags. The input hides at the limit.
suggestionsstring[][]Autocomplete suggestions surfaced via a <datalist>.
allowCustombooleantrueWhen false, only values from suggestions can be added.
transform"none" | "lowercase" | "uppercase" | "trim""none"Normalize tags as they’re added.
helpTextstringHelp text shown below the widget.

object-form and list accept an options.fields array of typed sub-field definitions. Each entry has a key (the JSON object key it writes to), a label, a type, and type-specific extras.

Sub-field typeRenders asNotable extras
textSingle-line inputplaceholder
textareaMulti-line inputrows (default 3), placeholder
numberNumeric inputmin, max, step, prefix, suffix, placeholder
booleanToggle switch
selectDropdownoptions: string[] | Array<{ label, value }>, placeholder
dateDate input
colorNative color picker paired with a hex text input
urlURL input (HTML5 type="url")placeholder

Common props on every sub-field: required, helpText, defaultValue.

The list widget renders each collapsed row using a Mustache-style template in options.summary. {{key}} is replaced with the row’s value for that key (coerced to a string). Falsy values fall back to "{itemLabel} {n}".

"summary": "{{name}} — {{amount}}"

Renders rows like Flour — 500g. The template is plain string substitution — no HTML, no nested expressions.

Field Kit widgets store plain JSON in the field’s existing column. There are no plugin-specific tables, no foreign keys, no schema mutation. If you remove @emdash-cms/plugin-field-kit from your config, the data stays valid — only the editing UI changes back to the default json text input.

This applies even when you change the widget shape: unknown keys on stored objects are preserved on the next write, so you can evolve a schema without losing data captured under an older field set.