Generating Forms with AI
A form here is a plain JSON array (a FormKit schema), so a language model can write it from a description and FUDataEdit renders it as is. This page has a prompt that does that reliably, how to check the result, and the mistakes models make without the prompt's rules.
How to use it
- Paste the prompt into your AI assistant and give it the framework docs it names. This site publishes them in a form an assistant can read:
llms.txt: an index with a link to a plain Markdown copy of every page. Point an assistant that can browse at it, or pick the pages the prompt names (Input Components, Schema-Based Forms, Known Limitations, plus Repeater or Multi-Step).llms-full.txt: every page in one file, about 170 KB at the time of writing. Use it only if your assistant has a large context window.
- Describe the form in plain words, for example "a hotel booking form: guest details, a room type, check-in and check-out dates, an extra-guests field only for family rooms and suites, a breakfast switch and a special requests box".
- Take the JSON it returns, render it and look at it.
- Ask for changes in words: "put the dates side by side", "add a step for payment", "make the phone number optional".
The prompt
You generate form definitions for the `@sfxcode/nuxt-ui-formkit` framework: FormKit schemas rendered with Nuxt UI components.
## Reference
Read these framework docs first and use only what they document: Input Components (every field type and its props), Schema-Based Forms (schema basics and conditional fields), Known Limitations (silent failures to avoid), plus Repeater for lists and Multi-Step for wizards.
## Output
Return ONE valid JSON array and nothing else: no prose, no comments, no trailing commas, no functions (so no `new Date()`). It is the `schema` of `<FUDataEdit :schema="..." />`, which adds the Save button itself.
## Node kinds
- A field: `{ "$formkit": "<type>", "name": "<dataKey>", "label": "...", ...props }`
- Plain HTML, for headings and layout: `{ "$el": "h2", "attrs": { "class": "text-xl font-semibold" }, "children": ["Title"] }`
- A node is shown conditionally with `"if": "<expression>"`.
## Rules
1. Only use field types and props the docs list. Never invent a type or a prop; an unknown prop may be silently ignored.
2. Every field has a unique camelCase `name` and a human `label`. Add `help` or `placeholder` where it helps the user.
3. Validation is a pipe string of FormKit rules: `"required|email"`, `"required|length:8,64"`, `"required|confirm"`. For a checkbox or switch that must be on, use `accepted`, not `required`.
4. Conditional fields: `"if": "$get(plan).value === 'pro'"`. `$get()` finds a field by its **`id`**, not its `name`, so the field it reads needs an `id` (use the same string as its `name`). `&&`, `||`, `===` and `!==` work. There is no unary `!`: write `!== true`, not `=== false`, because an untouched checkbox or switch is `undefined`, not `false`, unless you give it `"value": false`.
5. Inside a repeater row, read a sibling field with `$node.value.<field>` instead of `$get()`.
6. Choice fields: `nuxtUIRadioGroup` for one of a few options, `nuxtUICheckboxGroup` for several of a few, `nuxtUISelect` for one of many, `nuxtUISelectMenu` with `"multiple": true` for several of many. Options are strings or `{ "label": "...", "value": "..." }`; prefer the object form with a stable camelCase `value`.
7. Dates: `nuxtUIInputDate` with `"valueType": "iso"` so the value is a plain string.
8. Sliders and rating inputs always have a value: give a sensible default with `"value"`.
9. Layout: a `$el` heading per section, and short fields side by side in a grid wrapper: `{ "$el": "div", "attrs": { "class": "grid grid-cols-1 sm:grid-cols-2 gap-4" }, "children": [ ...fields ] }`.
10. Repeaters (`nuxtUIRepeater`) are bare by default. Always set these, otherwise the insert button disappears after the first row, the row buttons stack, and only the move buttons show:
`"newItem": { ...every child name with an empty value }`, `"alwaysDisplayInsertButton": true`, `"displayDeleteButton": true`, `"displayCloneButton": true`, `"listClass": "grid gap-3"`, `"listItemClass": "flex items-start gap-2"`, `"buttonGroupClass": "mt-6 flex gap-1"`, `"insertButtonLabel": "Add <thing>"`, plus `minItems`/`maxItems` where they fit. Size the fields in a row with `"outerClass": "w-48"` (and `"class": "w-full"` on the field itself).
11. Wizards: one `nuxtUIMultiStep` (`"allowIncomplete": false`) whose `children` are `nuxtUIStep` nodes, each with a `name`, a `label` and its fields as `children`.
12. If the request is vague, make sensible choices rather than asking: group the fields into 2 to 4 sections, mark the essential ones `required`.2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Check the result
Never ship generated JSON unseen. Render it:
<script setup lang="ts">
const schema = [/* paste the generated array here */]
</script>
<template>
<FUDataEdit :data="{}" :schema="schema" debug-data @data-saved="console.log($event)" />
</template>2
3
4
5
6
7
The playground's Playground page (/playground) has a JSON editor that re-renders the form when you paste a schema into it, which is a quick first look. Then run through this list:
- Every conditional field: change the field it depends on and check that it appears and disappears. A condition that never matches shows no error; the field is just never there.
- Repeaters: add, copy, remove and reorder a few rows, and check the insert button is still there afterwards.
- Validation: submit the empty form and read the messages.
- The data: open Debug Data and check the keys and value types (dates as strings, switches as booleans).
- A wizard: press Next on an empty step; it should stay put.
Mistakes the prompt prevents
These came from rendering generated forms or from Known Limitations, and each has a rule in the prompt.
| Without the rule | What happens |
|---|---|
$get(userType) on a field that has only a name | The condition never matches and the field is never shown, with no error. $get() resolves by id (rule 4). |
| A repeater without the display flags | The insert button vanishes after the first row, the row buttons stack vertically, and delete and copy are missing (rule 10). |
if: '!$value' | Silently ignored; the expression language has no unary ! (rule 4). |
$get() inside a repeater row | Does not find the sibling field; use $node.value.<field> (rule 5). |
required on a checkbox or switch | Passes when it is false; use accepted (rule 3). |
A date field without valueType | The value is a CalendarDate object, which is not JSON (rule 7). |
| A made-up prop | May be ignored without a warning (rule 1). |
What was tested
Five forms were generated by models that were given only the prompt and the docs folder, then rendered and exercised in the playground. The first four used an early draft of the prompt; the hotel form used the final one above.
| Request | Result |
|---|---|
| Support request, with a phone field shown for urgent priority | Rendered; the conditional appeared when Urgent was chosen |
| Conference registration, with in-person-only fields | Rendered; the conditional fields appeared for In person |
| Job application with a work-history repeater | Rendered, but with the first prompt the repeater had no insert button after the first row and stacked buttons. Rule 10 fixes that. |
| Customer feedback survey (a deliberately vague request) | Rendered, with sensible sections |
| Hotel booking, with a condition using ` |
The wizard and the repeater with a per-row condition were written by hand from the prompt's rules and the docs rather than generated by a model, and they rendered and behaved as described: a row's End date hid when that row's Currently here switch was on, and Next did nothing on an empty step.
This tests the prompt, not any particular model: results vary, which is why the checks above matter.
An example
For the hotel request above, the generated schema was (unedited):
[
{
"$el": "h2",
"attrs": {
"class": "text-xl font-semibold"
},
"children": [
"Guest Details"
]
},
{
"$formkit": "nuxtUIInput",
"id": "guestName",
"name": "guestName",
"label": "Guest name",
"placeholder": "Full name",
"validation": "required"
},
{
"$el": "div",
"attrs": {
"class": "grid grid-cols-1 sm:grid-cols-2 gap-4"
},
"children": [
{
"$formkit": "nuxtUIInput",
"id": "email",
"name": "email",
"label": "Email",
"inputType": "email",
"placeholder": "you@example.com",
"leadingIcon": "i-lucide-mail",
"validation": "required|email"
},
{
"$formkit": "nuxtUIInput",
"id": "phone",
"name": "phone",
"label": "Phone",
"inputType": "tel",
"placeholder": "+1 555 123 4567",
"leadingIcon": "i-lucide-phone",
"validation": "required"
}
]
},
{
"$el": "h2",
"attrs": {
"class": "text-xl font-semibold mt-6"
},
"children": [
"Stay"
]
},
{
"$formkit": "nuxtUIRadioGroup",
"id": "roomType",
"name": "roomType",
"label": "Room type",
"options": [
{
"label": "Single",
"value": "single"
},
{
"label": "Double",
"value": "double"
},
{
"label": "Family",
"value": "family"
},
{
"label": "Suite",
"value": "suite"
}
],
"validation": "required"
},
{
"$el": "div",
"attrs": {
"class": "grid grid-cols-1 sm:grid-cols-2 gap-4"
},
"children": [
{
"$formkit": "nuxtUIInputDate",
"id": "checkIn",
"name": "checkIn",
"label": "Check-in",
"valueType": "iso",
"validation": "required"
},
{
"$formkit": "nuxtUIInputDate",
"id": "checkOut",
"name": "checkOut",
"label": "Check-out",
"valueType": "iso",
"validation": "required"
}
]
},
{
"$formkit": "nuxtUIInputNumber",
"id": "additionalGuests",
"name": "additionalGuests",
"label": "Additional guests",
"min": 0,
"max": 6,
"step": 1,
"value": 0,
"if": "$get(roomType).value === 'family' || $get(roomType).value === 'suite'"
},
{
"$el": "h2",
"attrs": {
"class": "text-xl font-semibold mt-6"
},
"children": [
"Extras"
]
},
{
"$formkit": "nuxtUISwitch",
"id": "breakfast",
"name": "breakfast",
"label": "Include breakfast",
"help": "Served daily from 7:00 to 10:30"
},
{
"$formkit": "nuxtUITextarea",
"id": "specialRequests",
"name": "specialRequests",
"label": "Special requests",
"rows": 4,
"autoresize": true,
"placeholder": "Anything we should know?"
}
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
A skill for your coding assistant
The prompt above is for generating one form. If an assistant is working in your project more broadly (setting the module up, writing and fixing forms), give it the skill that ships with the package. It is a short list of the rules that are easy to get wrong and fail silently: registering the inputs, $get() needing an id, the bare repeater defaults, field widths, and a pointer to llms.txt for everything else.
Because it is installed with the package, it always matches the version you have. For Claude Code, copy it into your project's skills folder:
mkdir -p .claude/skills
cp -r node_modules/@sfxcode/nuxt-ui-formkit/skills/nuxt-ui-formkit .claude/skills/2
Re-run the copy after you upgrade the module. For another assistant, the skill is plain Markdown (skills/nuxt-ui-formkit/SKILL.md): paste it into that tool's project instructions.
How the skill was tested
It was tried in a fresh Claude Code session, in a project that had the skill installed, and compared with a control session without it. Both sessions got the same request, which contained the usual traps: "a select userType (individual or business); a companyName field shown only for business users; a checkbox newsletter; a newsletterEmail field shown only when it is ticked; and a list of phone numbers where the user can add, remove and copy lines". Neither session could read files or browse; the skill session could only load the skill.
The skill was found: the session listed nuxt-ui-formkit among its skills. The two answers differed like this:
| Check | With the skill | Control |
|---|---|---|
| Field types | nuxtUICheckbox, nuxtUIInput, nuxtUISelect, nuxtUIRepeater | text, tel, select, checkbox, repeater (plain FormKit types, not this framework's) |
id on the fields read by $get() | on userType and newsletter | missing on both |
| Condition on the checkbox | $get(newsletter).value === true | the same |
| Repeater display flags and row layout | all set | no nuxtUIRepeater at all |
Read this as a sign that the skill loads and is followed, not as a measurement:
- It was one run each, and model output varies.
- The control had no docs to read, so the comparison mostly shows that the skill supplies the framework's type names and rules. It does not show the skill beating an assistant that can read this site.
- The request was written to contain the traps.
- The setup rules (registering the inputs in
formkit.config.ts) were not tested, because they need a real Nuxt project.
Tips
- Name the fields you care about. A vague request ("a feedback survey") gets sensible defaults; a precise one gets what you asked for.
- Say which fields are conditional, and what they depend on, in the request.
- Keep the docs attached in follow-up messages, or the assistant may fall back on guesses.
- Treat the output as a draft. The prompt makes the schema valid, not right for your product: check the labels, the validation and the data keys.
Known gaps
- Date inputs keep their compact width inside a grid, next to full-width text fields. See Styling.
- Whether a field hidden by
ifkeeps a value in the data is not documented; check Debug Data if it matters to you. - There is no cross-field rule such as "check-out after check-in" in a JSON schema.