---
title: "Custom form fields"
description: "Extend FlowDrop's form system with custom field components."
source: https://flowdrop.io/docs/editor/custom-form-fields
site: FlowDrop documentation
---
# Custom form fields
FlowDrop generates configuration forms automatically from JSON Schema. The field registry system lets you add custom field components — for example a color picker, date picker, or rich text editor.
## Quick start
**1. Write a Svelte field component:**
```svelte
onChange(e.currentTarget.value)}
/>
```
**2. Register it:**
```typescript
import { getInstance } from '@flowdrop/flowdrop/editor';
import ColorPickerField from './ColorPickerField.svelte';
const fd = getInstance(); // or app.instance outside the component tree
fd.fields.register('color-picker', {
component: ColorPickerField,
matcher: (schema) => schema.format === 'color',
priority: 100
});
```
**3. Use it in a config schema:**
```json
{
"accentColor": {
"type": "string",
"format": "color",
"title": "Accent Color",
"default": "#3b82f6"
}
}
```
## How it works
When `FormFieldLight` renders a field, it:
1. Calls `resolveFieldComponent(schema)` to check the registry
2. If a registered matcher returns `true`, renders the registered component
3. Otherwise falls back to built-in fields (text, number, toggle, select, etc.)
Registrations are **priority-ordered** — higher priority matchers are checked first.
## Field component props
Your component receives these props:
```typescript
interface Props {
id: string;
value: unknown;
placeholder?: string;
required?: boolean;
ariaDescribedBy?: string;
onChange: (value: unknown) => void;
}
```
Only the props listed above are guaranteed. Read any additional schema properties (like `schema.minDate`) directly from the schema via the form context — see [Reading sibling field values](#reading-sibling-field-values) below.
## Matcher functions
A matcher decides whether your component handles a given schema:
```typescript
// Match by format
(schema) => schema.format === "color"
// Match by type + format
(schema) => schema.type === "string" && schema.format === "rich-text"
// Match by custom property
(schema) => schema.widget === "my-widget"
```
## Priority-based resolution
When multiple registrations match, the highest priority wins:
```typescript
// Priority 50 — general fallback
fd.fields.register('text-basic', {
component: BasicTextField,
matcher: (schema) => schema.type === 'string',
priority: 50
});
// Priority 100 — more specific, checked first
fd.fields.register('rich-text', {
component: RichTextField,
matcher: (schema) => schema.type === 'string' && schema.format === 'rich-text',
priority: 100
});
```
You can use this to **override built-in fields** by registering your own component with a higher priority.
## Lazy registration
For heavy dependencies, use dynamic imports:
```typescript
import type { FieldComponentRegistry } from '@flowdrop/flowdrop/form';
export function registerMyHeavyField(fields: FieldComponentRegistry, priority = 100): void {
if (fields.has('my-heavy-field')) return;
import('./MyHeavyField.svelte').then((module) => {
fields.register('my-heavy-field', {
component: module.default,
matcher: (schema) => schema.format === 'heavy',
priority
});
});
}
// Call with the instance's registry: registerMyHeavyField(getInstance().fields)
```
## Built-in field types
These fields are always available without registration:
| Schema | Renders as |
| --------------------------------------------- | --------------------------- |
| `type: "string"` | Text input |
| `type: "string", format: "multiline"` | Textarea |
| `type: "number"` or `type: "integer"` | Number input |
| `type: "number", format: "range"` | Range slider |
| `type: "boolean"` | Toggle switch |
| `type: "string", enum: [...]` | Select dropdown |
| `type: "string", enum: [...], multiple: true` | Checkbox group |
| `type: "string", oneOf: [{const, title}]` | Select with labeled options |
| `type: "array", items: {...}` | Dynamic list |
| `format: "hidden"` | Hidden (not rendered) |
These require explicit registration (heavy dependencies):
Each installer takes the target field registry (`fd.fields`) as its first argument:
| Schema | Import path | Registration function |
| ------------------------------------ | ---------------------------------- | ---------------------------------------- |
| `format: "json"` or `format: "code"` | `@flowdrop/flowdrop/form/code` | `registerCodeEditorField(fd.fields)` |
| `format: "template"` | `@flowdrop/flowdrop/form/code` | `registerTemplateEditorField(fd.fields)` |
| `format: "markdown"` | `@flowdrop/flowdrop/form/markdown` | `registerMarkdownEditorField(fd.fields)` |
## Field management
Field management is done through the instance's `fd.fields` registry (a `FieldComponentRegistry`):
```typescript
import { getInstance } from '@flowdrop/flowdrop/editor';
const fd = getInstance(); // or app.instance outside the component tree
fd.fields.unregister('color-picker'); // returns boolean
fd.fields.getKeys(); // ["color-picker", ...]
fd.fields.has('color-picker'); // true or false
fd.fields.size; // number of registrations
fd.fields.clear(); // clear all (useful in tests)
```
## Reading sibling field values
Custom components registered for `format: "autocomplete"` fields receive the full `schema` object as a prop and can read the current values of other fields in the same form using the `FORM_VALUES_KEY` context.
This is the building block for dependent autocomplete fields — for example a `project` field whose suggestions depend on the currently selected `account`.
**1. Define the schema** — use any custom property to declare the dependency:
```json
{
"account": { "type": "string", "title": "Account" },
"project": {
"type": "string",
"format": "autocomplete",
"title": "Project",
"autocomplete": { "url": "/api/projects", "labelField": "name", "valueField": "id" },
"dependencies": { "account": "account" }
}
}
```
**2. Write the component** — wrap `FormAutocomplete` and patch the URL:
```svelte
```
**3. Register it** — match on the custom `dependencies` property:
```typescript
import { getInstance } from '@flowdrop/flowdrop/editor';
import DependentAutocomplete from './DependentAutocomplete.svelte';
const fd = getInstance(); // or app.instance outside the component tree
fd.fields.register('dependent-autocomplete', {
component: DependentAutocomplete,
matcher: (schema) => schema.format === 'autocomplete' && 'dependencies' in schema,
priority: 150
});
```
The registered component is only activated when a schema has both `format: "autocomplete"` and a `dependencies` property. All other autocomplete fields continue to use FlowDrop's built-in `FormAutocomplete`.
**`FormAutocomplete` is a named export.**
Import it with `import { FormAutocomplete } from '@flowdrop/flowdrop/form/autocomplete'`.