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.
Installation
Section titled “Installation”npm i @emdash-cms/plugin-field-kitRegister 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": [...] }}Widgets
Section titled “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
Section titled “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.
{ "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. |
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 }]| 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. |
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 }}| 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. |
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.
| 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
Section titled “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
Section titled “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}".
"summary": "{{name}} — {{amount}}"Renders rows like Flour — 500g. The template is plain string substitution — no HTML, no nested expressions.
Data durability
Section titled “Data durability”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.
See also
Section titled “See also”- Plugin Overview — how EmDash plugins work.
- Creating Plugins — write your own field widgets if Field Kit doesn’t fit.
- Discussion #571 — the proposal that led to this plugin.