FEATURE · DATA
Collect structured data with every shape you draw.
Define typed fields once. Element generates the form, validates the input, and writes the result into standard GeoJSON. Schemas travel with your data.
Schema API and in-app schema editor ship in every package, free under MIT.

Try it
Draw a shape. Fill in the form. Watch the GeoJSON update live.
- 1Click a café, press ⌘E to see its form
- 2Press P, click a corner, press ⌘E, fill it in
- 3Press S to open the schema and add a field, I to see the GeoJSON
GeoJSON's properties object is an untyped bag
The RFC 7946 spec gives every feature a properties object — but makes no claims about what goes in it. Every codebase reinvents the same loop: validate fields in the frontend, validate them again in the backend, hope the two agree.
Element adds a schema layer. Define fields once — typed, optionally required, with per-type validation — and Element generates the editing form, enforces the constraints at edit time, and writes the result into standard properties. The schemas themselves travel with the data as a foreign member on the FeatureCollection.
What you get
Typed fields. text, textarea, number, url, select, date, boolean — each with a matching input widget.
Per-type validation. Required fields. Min and max on numbers. Options list on selects. Validation runs on each edit, and errors show inline in the form.
Multiple schemas per collection. Different features can use different schemas. A "Building" schema and a "Route" schema live side by side; each feature carries its own schemaId.
In-app schema editor. Press S to open the schema editor. Add fields, rename them, reorder them, change types. Changes propagate to every feature using the schema, without overwriting existing property values.
Non-destructive schema edits. Rename a field, delete a field — existing property values are preserved on the feature. Element never silently drops data.
Ships in the GeoJSON. Schemas export as metadata.schemas on the FeatureCollection. On import, they're restored. Your data is portable.
Define a schema. Drop it in.
import { ElementEditor } from '@element-editor/react'
import type { Schema } from '@element-editor/react'
const propertySchema: Schema = {
id: 'property',
name: 'Property',
fields: [
{ key: 'owner', label: 'Owner', type: 'text', required: true },
{ key: 'area_sqm', label: 'Area (m²)', type: 'number', required: false, min: 0 },
{ key: 'zoning', label: 'Zoning', type: 'select', required: false,
options: ['residential', 'commercial', 'mixed'] },
{ key: 'surveyed_at', label: 'Surveyed', type: 'date', required: false },
{ key: 'notes', label: 'Notes', type: 'textarea', required: false },
],
}
export default function App() {
return (
<div style={{ height: 600 }}>
<ElementEditor
schema={propertySchema}
onValidationError={(errors) => console.warn(errors)}
/>
</div>
)
}The schema prop pre-loads a single schema. To pre-load multiple, include them on
initialData.metadata.schemas. End users can also create and edit schemas at runtime via the schema editor (S).

Field types at a glance
| Type | Input widget | Options | Validates |
|---|---|---|---|
text | Single-line text input | — | required |
textarea | Multi-line text area | — | required |
number | Numeric input | min, max | required, range |
url | URL input | — | required |
select | Dropdown | options: string[] | required |
date | Native date picker | — | required |
boolean | Checkbox | — | — |
All field values live in the feature's properties object under the field's key. Fields added in the schema editor get a key slugged from the label — "Floor area" becomes floor_area.
What it looks like in the GeoJSON
Schemas attach to the FeatureCollection as a foreign member under metadata.schemas. Each feature carries a schemaId pointing to one of them. Property values live in the standard properties object — no custom wrappers, no special types.
{
"type": "FeatureCollection",
"metadata": {
"schemas": [
{
"id": "property",
"name": "Property",
"fields": [
{ "key": "owner", "label": "Owner", "type": "text", "required": true },
{ "key": "area_sqm", "label": "Area (m²)", "type": "number", "required": false, "min": 0 }
]
}
],
"groups": []
},
"features": [
{
"type": "Feature",
"id": "abc-123",
"schemaId": "property",
"geometry": {
"type": "Polygon",
"coordinates": [[[2.35, 48.86], [2.36, 48.86], [2.36, 48.87], [2.35, 48.86]]]
},
"properties": {
"owner": "Acme Corp",
"area_sqm": 1247
}
}
]
}metadata, schemaId, and groupId are RFC 7946 foreign members — standard JSON parsers keep them; strict GeoJSON consumers ignore them. Your data stays valid either way.
Things to know before you ship
- Schemas are code-defined and end-user editable. Pass one via the
schemaprop; pre-load multiple by including them oninitialData.metadata.schemas. Users can edit schemas at runtime via the schema editor (S). - Schema changes are non-destructive. Removing a field from a schema keeps the value on existing features — Element never silently drops data.
- There's one active schema per form. When a feature is selected, its
schemaIddetermines the form rendered. Features with different schemas coexist without conflict.
Works with
- Data table — every schema field becomes a column. Edit values inline. Rows sync with map selection.
- GeoJSON editor — see and edit your schemas and features on the wire, side by side with the map.
- Import and export — schemas travel on the FeatureCollection as
metadata.schemas. Round-trip safe.