| title | Field Kit |
|---|---|
| description | Composable field widgets for json fields, configured through seed options. |
import { Aside } from "@astrojs/starlight/components";
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.
Install the package from npm:
npm i @emdash-cms/plugin-field-kitThe 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": [...] }
}| 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.
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. |
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. |
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. |
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. |
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.
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.
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.
- 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.