EmDash’s json field type stores arbitrary structured data, edited by default through a single-line text input that takes raw JSON. Field Kit is a first-party plugin that ships four composable widgets for json fields, configured entirely through seed options so site builders can use them with seed schema alone.
Installation
Install the package from npm:
npm i @emdash-cms/plugin-field-kit
The following configuration registers the plugin:
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { fieldKitPlugin } from "@emdash-cms/plugin-field-kit";
export default defineConfig({
integrations: [
emdash({
plugins: [fieldKitPlugin()],
}),
],
});
Attach a widget to any json field by setting widget to field-kit:<name>. The following field definition uses the list widget:
{
"slug": "ingredients",
"type": "json",
"widget": "field-kit:list",
"options": { "fields": [...] }
}
Widgets
| Widget | Use for | Stored value |
|---|---|---|
object-form | Inline form for flat JSON objects | { key: value, ... } |
list | Ordered array editor with add / remove / reorder | [{ ... }, ...] |
grid | Rows × columns matrix | { rowKey: { colKey: value } } |
tags | Free-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.
object-form
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. The following field definition configures a nutrition object:
{
"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 }.
| Option | Type | Default | Description |
|---|---|---|---|
fields | SubFieldDef[] | (required) | Sub-field definitions — see Sub-fields. |
collapsed | boolean | false | Render the group collapsed by default. |
helpText | string | — | Help text shown below the widget. |
list
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. The following field definition configures an ingredients list:
{
"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" }
]
}
}
The stored value is an array of row objects:
[
{ "name": "Flour", "amount": "500g", "optional": false },
{ "name": "Butter", "amount": "200g", "optional": false }
]
| Option | Type | Default | Description |
|---|---|---|---|
fields | SubFieldDef[] | (required) | Sub-field definitions for each row. |
itemLabel | string | "Item" | Singular label for a row (used in the “Add” button and fallback row titles). |
min | number | — | Minimum number of items. Below this, the remove button hides. |
max | number | — | Maximum number of items. At this count, the add button hides. |
sortable | boolean | true | Show up/down reorder buttons. |
summary | string | — | Mustache template rendered as the collapsed-row title. See Summary templates. |
helpText | string | — | Help text shown below the widget. |
grid
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. The following field definition configures a seasonal availability grid:
{
"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" }
]
}
}
The stored value is an object keyed by row, then by column:
{
"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 }
}
| Option | Type | Default | Description |
|---|---|---|---|
rows | GridAxisDef[] | (required) | Row definitions: { key, label, image? }. |
columns | GridAxisDef[] | (required) | Column definitions: { key, label, image? }. |
cell | "toggle" | "text" | "number" | "select" | "toggle" | Cell input type, applied uniformly to every cell. |
cellOptions | string[] | Array<{ label, value }> | [] | Required when cell is "select". |
helpText | string | — | Help text shown below the widget. |
tags
A chip-style input for arrays of strings. Supports a fixed suggestions list, free-form custom values (toggleable), case transforms, and an optional max. The following field definition configures a keywords tag input:
{
"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.
| Option | Type | Default | Description |
|---|---|---|---|
placeholder | string | "Add..." | Input placeholder shown when no tags are present. |
max | number | — | Maximum number of tags. The input hides at the limit. |
suggestions | string[] | [] | Autocomplete suggestions surfaced via a <datalist>. |
allowCustom | boolean | true | When false, only values from suggestions can be added. |
transform | "none" | "lowercase" | "uppercase" | "trim" | "none" | Normalize tags as they’re added. |
helpText | string | — | Help text shown below the widget. |
Sub-fields
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 type | Renders as | Notable extras |
|---|---|---|
text | Single-line input | placeholder |
textarea | Multi-line input | rows (default 3), placeholder |
number | Numeric input | min, max, step, prefix, suffix, placeholder |
boolean | Toggle switch | — |
select | Dropdown | options: string[] | Array<{ label, value }>, placeholder |
date | Date input | — |
color | Native color picker paired with a hex text input | — |
url | URL input (HTML5 type="url") | placeholder |
Common props on every sub-field: required, helpText, defaultValue.
Summary templates
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}". The following template combines two keys:
"summary": "{{name}} — {{amount}}"
Renders rows like Flour — 500g. The template is plain string substitution — no HTML, no nested expressions.
Data durability
Field Kit widgets store plain JSON in the field’s existing column, using only that column. If you remove @emdash-cms/plugin-field-kit from your config, the data stays valid — the field reverts 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.
See also
- Plugin Overview — how EmDash plugins work.
- Choosing a plugin format — write your own field widgets if Field Kit doesn’t fit.
- Discussion #571 — the proposal that led to this plugin.