# Nuxt UI FormKit > FormKit integration for Nuxt UI - Seamlessly connect FormKit form handling with Nuxt UI components --- # Installation Get started with Nuxt UI FormKit by installing the module in your Nuxt application. ## Prerequisites Before you begin, make sure you have the following installed: - **Node.js** - a version supported by Nuxt 4 - **Nuxt** 4.x - **pnpm**, **npm**, or **yarn** package manager ## Install Package Install the `@sfxcode/nuxt-ui-formkit` module using your preferred package manager: ::: code-group ```bash [pnpm] pnpm add @sfxcode/nuxt-ui-formkit ``` ```bash [npm] npm install @sfxcode/nuxt-ui-formkit ``` ```bash [yarn] yarn add @sfxcode/nuxt-ui-formkit ``` ::: ## Module Configuration Add the module to your `nuxt.config.ts`: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: [ '@nuxt/ui', '@formkit/nuxt', '@sfxcode/nuxt-ui-formkit' ] }) ``` ::: info **Note:** Make sure to add `@nuxt/ui` before `@sfxcode/nuxt-ui-formkit` in the modules array. ::: ::: warning `@formkit/nuxt` is required in `modules` This module does not register `@formkit/nuxt` for you. It ships as a dependency, but it must be listed in `modules` so Nuxt picks up your `formkit.config.ts` (below). Without it the `nuxtUI*` input types are never registered. ::: ## FormKit Configuration Create a `formkit.config.ts` file in your project root to configure FormKit. The recommended way is `createNuxtUiFormkitConfig()`, which registers every `nuxtUIXxx` input/output (and its `form` name) plus this module's own plugins for you - spread its result into your own config alongside whatever else you need (locales, `iconLoader`, other plugins): ```typescript [formkit.config.ts] import type { DefaultConfigOptions } from '@formkit/vue' import { en } from '@formkit/i18n' import { createAutoAnimatePlugin, createNuxtUiFormkitConfig } from '@sfxcode/nuxt-ui-formkit/formkit' // `multiStep: true` additionally registers `createMultiStepPlugin()` - // omit it if you're not using `nuxtUIMultiStep`/`nuxtUIStep`. // Pass your own plugins through the `plugins` option: they are appended after // this module's, so the asterisk plugin is kept. const nuxtUiFormkitConfig = createNuxtUiFormkitConfig({ multiStep: true, plugins: [ createAutoAnimatePlugin( { duration: 250, easing: 'ease-in-out', }, { global: ['outer', 'inner'], form: ['form'], nuxtUIRepeater: ['input'], } ), ], }) const config: DefaultConfigOptions = { locales: { en }, locale: 'en', inputs: nuxtUiFormkitConfig.inputs, plugins: nuxtUiFormkitConfig.plugins, } export default config ``` ### The `form` prefix Every input and output is also available under the library-neutral `form` prefix: `formInput` next to `nuxtUIInput`, `formSelect` next to `nuxtUISelect`, `formRepeater` next to `nuxtUIRepeater`, `formOutputText` next to `nuxtUIOutputText`, and so on. The `form` names are the same definitions, so both names behave identically. Use them when a schema should keep its type names across UI libraries that offer the same prefix, such as [FormKit PrimeVue](https://github.com/sfxcode/formkit-primevue). `createNuxtUiFormkitConfig()` registers the `form` names next to the `nuxtUI` names, which are unchanged. If an input of your own happens to be called `formSelect` and must not be replaced, register only the `nuxtUI` names with `formPrefix: false`: ```typescript [formkit.config.ts] const nuxtUiFormkitConfig = createNuxtUiFormkitConfig({ formPrefix: false }) ``` ```typescript const schema = [ { $formkit: 'formInput', name: 'name', label: 'Name', validation: 'required' }, { $formkit: 'formSelect', name: 'role', label: 'Role', options: ['Tank', 'Healer'] }, ] ``` The asterisk plugin, `createMultiStepPlugin`, the TypeScript types and the animation targets of `createAutoAnimatePlugin` treat the `form` names like the `nuxtUI` names: a target keyed `nuxtUIRepeater` also applies to `formRepeater`, and the other way round. ::: details Escape hatch: fully manual wiring If you need finer-grained control (e.g. registering only a subset of inputs), you can still wire everything up yourself instead of using the helper: ```typescript [formkit.config.ts] import type { DefaultConfigOptions } from '@formkit/vue' import { en } from '@formkit/i18n' import { nuxtUIInputs, nuxtUIOutputs } from '@sfxcode/nuxt-ui-formkit/definitions' import { addNuxtAsteriskPlugin } from '@sfxcode/nuxt-ui-formkit/plugins' const config: DefaultConfigOptions = { locales: { en }, locale: 'en', inputs: { ...nuxtUIInputs, ...nuxtUIOutputs }, plugins: [addNuxtAsteriskPlugin], } export default config ``` ::: ## Verify Installation Create a test page to verify the installation: ```vue [app.vue] ``` Start your development server: ```bash pnpm dev ``` Navigate to your application and you should see a fully functional form with Nuxt UI styling! ✨ ## Next Steps ### πŸš€ Quick Start Learn the basics and create your first form [Go to Quick Start β†’](/getting-started/quick-start) ### 🧩 Components Explore all available input and output components [Browse Components β†’](/components/inputs) ### πŸ’‘ Examples View real-world form examples and use cases [See Examples β†’](/examples/) ### πŸ–₯️ Server-Side Rendering What is rendered on the server, what appears after hydration, and the one thing to avoid in a page's dates [Read about SSR β†’](/getting-started/ssr) --- # Quick Start Learn the basics of Nuxt UI FormKit using schema-based forms and create your first form in minutes. ## Your First Schema-Based Form Nuxt UI FormKit is designed around **schema-based forms** - a powerful pattern where you define your form structure as a JSON object. This approach provides better maintainability, reusability, and type safety. Create a new page in your Nuxt application: ```vue [pages/contact.vue] ``` ## Understanding Schema-Based Forms ### Why Schemas? Schemas are **first-class citizens** in Nuxt UI FormKit: - βœ… **Maintainable** - Form structure separated from template code - βœ… **Reusable** - Share schemas across components and projects - βœ… **Type-safe** - Full TypeScript support and IntelliSense - βœ… **Dynamic** - Generate forms from API responses or config files - βœ… **Composable** - Build complex forms from smaller schemas - βœ… **Testable** - Easy to test form logic without rendering ### The Schema Pattern Every schema is an array of field definitions: ```typescript const schema = [ { $formkit: 'nuxtUIInput', // Component type name: 'fieldName', // Field name in data object label: 'Field Label', // Display label validation: 'required', // Validation rules // ... other component props } ] ``` ### FUDataEdit Component The `FUDataEdit` component is your main tool for schema-based forms: ```vue :schema="schema" :debug-data="false" @data-saved="handleSubmit" /> ``` **Key Features:** - Automatic two-way binding with `data` - Built-in form wrapper and submit handling - Validation support out of the box - Optional debug panel for development ## Building Your Schema ### Basic Field Types ```typescript const userSchema = [ // Text input { $formkit: 'nuxtUIInput', name: 'username', label: 'Username', validation: 'required|length:3' }, // Email input { $formkit: 'nuxtUIInput', name: 'email', inputType: 'email', label: 'Email', validation: 'required|email' }, // Number input { $formkit: 'nuxtUIInputNumber', name: 'age', label: 'Age', min: 0, max: 120 }, // Select dropdown { $formkit: 'nuxtUISelect', name: 'country', label: 'Country', options: ['USA', 'UK', 'Canada', 'Australia'] }, // Switch toggle { $formkit: 'nuxtUISwitch', name: 'newsletter', label: 'Subscribe to newsletter' } ] ``` ### Organizing with Sections Use HTML elements to structure your form: ```typescript const schema = [ // Section heading { $el: 'h3', children: 'Personal Information', attrs: { class: 'text-xl font-bold mb-4' } }, { $formkit: 'nuxtUIInput', name: 'firstName', label: 'First Name' }, { $formkit: 'nuxtUIInput', name: 'lastName', label: 'Last Name' }, // Another section { $el: 'h3', children: 'Contact Details', attrs: { class: 'text-xl font-bold mt-6 mb-4' } }, { $formkit: 'nuxtUIInput', name: 'email', inputType: 'email', label: 'Email' } ] ``` ## Complete Example Here's a comprehensive user registration form: ```vue ``` ## Validation FormKit's validation integrates seamlessly with schemas: ```typescript const schema = [ { $formkit: 'nuxtUIInput', name: 'email', label: 'Email', validation: 'required|email', validationMessages: { required: 'Email is required', email: 'Please enter a valid email' } } ] ``` **Common Rules:** - `required` - Must have value - `email` - Valid email format - `length:min,max` - String length - `min:n` / `max:n` - Numeric range - `matches:/pattern/` - Regex match - `confirm:field` - Match another field See [FormKit validation docs](https://formkit.com/essentials/validation) for complete list. ## Template-Based Forms (Alternative) While schemas are recommended, you can also use template-based forms: ```vue ``` ::: tip Best Practice Use schemas for most forms. Reserve template-based approach for simple cases or when you need fine-grained control over layout. ::: ## Next Steps ### πŸ“„ Schema Forms Deep Dive Advanced schema patterns and techniques [Learn More β†’](/getting-started/schema-forms) ### 🧩 Component Reference All available components with schema examples [Browse Components β†’](/components/inputs) ### πŸ”„ Repeaters Dynamic repeatable sections in schemas [View Repeaters β†’](/components/repeater) --- # Schema-Based Forms Learn how to build forms using JSON schemas for more maintainable and dynamic form generation. ## Why Use Schemas? Schema-based forms offer several advantages: - **Maintainability** - Separate form structure from component templates - **Reusability** - Share schemas across components or projects - **Dynamic** - Generate forms from API responses or configuration files - **Type Safety** - Full TypeScript support for schema definitions - **Consistency** - Enforce consistent form patterns across your application ## Basic Schema A schema is an array of field definitions: ```typescript const schema = [ { $formkit: 'nuxtUIInput', name: 'username', label: 'Username', validation: 'required|length:3' }, { $formkit: 'nuxtUIInput', name: 'email', inputType: 'email', label: 'Email Address', validation: 'required|email' } ] ``` ## Using FUDataEdit The `FUDataEdit` component renders a schema-based form: ```vue ``` ## FUDataView for Display Use `FUDataView` to display data in read-only mode: ```vue ``` ## Advanced Schema Features ### Conditional Fields Show fields based on other field values: ```typescript const schema = [ { $formkit: 'nuxtUISelect', id: 'userType', // `$get()` looks a field up by its `id` name: 'userType', label: 'User Type', options: ['individual', 'business'] }, { $formkit: 'nuxtUIInput', name: 'companyName', label: 'Company Name', if: '$get(userType).value === "business"' // Only show for business users } ] ``` ::: warning The referenced field needs an `id` `$get(userType)` resolves by `id`, not by `name`. Without the `id` the condition never matches and the field is silently never shown. Inside a repeater row, read a sibling with `$node.value.` instead (see [Known Limitations](/getting-started/known-limitations#repeaters-and-schemas)). ::: ### Grouped Fields Group related fields together: ```typescript const schema = [ { $el: 'h3', children: 'Personal Information' }, { $formkit: 'nuxtUIInput', name: 'firstName', label: 'First Name' }, { $formkit: 'nuxtUIInput', name: 'lastName', label: 'Last Name' }, { $el: 'h3', children: 'Contact Details', attrs: { class: 'mt-6' } }, { $formkit: 'nuxtUIInput', name: 'email', label: 'Email' } ] ``` ### Dynamic Options Load options dynamically: ```vue ``` ## Schema with Repeaters Create dynamic lists with the repeater: ```typescript const schema = [ { $formkit: 'nuxtUIRepeater', name: 'contacts', label: 'Contacts', newItem: { name: '', email: '' }, children: [ { $formkit: 'nuxtUIInput', name: 'name', label: 'Name' }, { $formkit: 'nuxtUIInput', name: 'email', inputType: 'email', label: 'Email' } ] } ] ``` ## TypeScript Support Define typed schemas for better IntelliSense: ```typescript import type { FormKitSchemaNode } from '@formkit/core' interface UserForm { name: string email: string age: number } const formData = ref({ name: '', email: '', age: 0 }) const schema: FormKitSchemaNode[] = [ { $formkit: 'nuxtUIInput', name: 'name', label: 'Name', validation: 'required' }, // ... more fields ] ``` ## Best Practices - βœ… Keep schemas in separate files for reusability - βœ… Use TypeScript interfaces for form data - βœ… Leverage computed properties for dynamic schemas - βœ… Group related fields with sections - βœ… Use conditional rendering for complex forms - βœ… Add clear labels and help text ## Next Steps ### πŸ”„ Repeaters Learn about dynamic repeatable sections [View Repeaters β†’](/components/repeater) ### πŸ’‘ Examples See real-world schema-based forms [Browse Examples β†’](/examples/) ### πŸ“š API Reference Complete API documentation [API Docs β†’](/api/utilities) --- # Server-Side Rendering The module works on a server-rendered page: every input and output type renders on the server and hydrates without a warning. This is checked, not assumed - the playground is loaded with server rendering on, in `nuxi dev` and in a production build, with the server and the browser deliberately in time zones 26 hours apart, and it fails on any hydration warning, console error, Vue warning or error in the server log (`npm run test:e2e:ssr` in the repository). Server rendering is off in the playground by default; set `PLAYGROUND_SSR=true` to turn it on. ## What the server renders | | On the server | After hydration | |---|---|---| | Field chrome (label, help text, messages) | rendered | unchanged | | Every input and output type | rendered | unchanged | | `nuxtUIEditor` | label and help text only | the editor, its toolbar and its content | | `nuxtUIMultiStep` / `nuxtUIStep` | the step panels and their fields; **not the tab strip** | the tab strip | | `FUDataEdit` `autosave` | the form, without a draft | a stored draft is restored into the inputs | | Date and calendar fields | the day in the **server's** time zone | the day in the **browser's** time zone | None of these cause a hydration mismatch. The rows that differ before and after hydration are described below. ## The editor `UEditor` is built in the browser, so the server leaves the editor's place empty and it appears after hydration. The content below an editor therefore moves down when it mounts (about 73px in the default theme). If that matters on your page, reserve the space with a minimum height on the field: ```ts { $formkit: 'nuxtUIEditor', name: 'body', label: 'Body', outerClass: 'min-h-[113px]' } ``` ## Multi-step forms The step panels and the fields in them are server-rendered, so the form is readable without JavaScript, but the tab strip is built in the browser. It appears right after hydration. ## Date and calendar fields A JavaScript `Date` or an ISO string with a time zone is an instant, not a calendar day: which day it shows depends on the zone it is shown in, and the server cannot know the browser's. So the server renders the day in its own zone, the browser hydrates with the same zone, and the field switches to the browser's own zone right after mount. A visitor in another zone sees the day change once, immediately after the page loads - there is no hydration mismatch, and the form value is not changed by it. To show the same day everywhere, give the field an explicit zone, which is never switched: ```ts { $formkit: 'nuxtUIInputDate', name: 'start', label: 'Start', valueType: 'date', timeZone: 'UTC' } ``` A calendar with no value is centred on today in the same zone, so it follows the same rule. A calendar or date field holding a plain day (`'2026-07-05'` or a `CalendarDate`) has no zone and is never affected. ::: warning Build dates from ISO strings or `Date.UTC`, not from local parts In a page that is server-rendered, the page's script runs twice - on the server and in the browser. `new Date(2026, 6, 5)` is *local* midnight, so it is a different instant in each place, and the field will hydrate with a different day. Use `new Date(Date.UTC(2026, 6, 5, 12))`, an ISO string, or a `CalendarDate`. ::: ## Overlays `useFormKitOverlay().edit()` and `.auto()` need a user to answer them, so they cannot run on the server: the promise would never settle and an `await` in an async `setup` would hang the request. They throw an error that says so. Call them from an event handler or `onMounted`; creating the composable itself is safe anywhere. ## Autosave `autosave` needs no special handling. On the server the plugin does nothing; in the browser it restores a stored draft after hydration, so the first render matches the server's and the draft appears as an ordinary update. ## Checking your own app Vue only compares attributes during hydration in a **development** build; a production build silently keeps whatever the server rendered. So check for hydration warnings with `nuxi dev`, where `Hydration attribute mismatch` and `Hydration text mismatch` are reported. If you add a page to the playground it is covered automatically. --- # Styling The module renders Nuxt UI components inside FormKit's field chrome, so a field is styled in two places: the chrome around it, and the Nuxt UI component inside it. This page collects what works well, taken from the playground (`npm run dev`), which applies all of it. ## How a field is built ``` .formkit-outer ← outerClass .formkit-wrapper label.formkit-label .formkit-inner ← the Nuxt UI component sits here .formkit-help .formkit-messages ``` Two schema props decide where a class lands: | Prop | Applies to | |---|---| | `outerClass` | the wrapper around the whole field (label, component, help, messages) | | `class` | the Nuxt UI component itself | Each `.formkit-outer` also carries `data-type` with the FormKit type name (`nuxtUIInput`, `nuxtUISlider`, ...). Use it to target one type in CSS. Two types keep the plain addons names: the wizard is `multi-step` and its panels are `step`. ## Field width Nuxt UI inputs and selects size themselves to their content and do not stretch to their wrapper. In a wide form that leaves narrow fields, and in a grid it leaves gaps next to the column edge. For one field, size the wrapper and make the field fill it: ```ts { $formkit: 'nuxtUIInput', name: 'title', label: 'Title', outerClass: 'w-64', class: 'w-full' } ``` `nuxtUISelect` and `nuxtUISelectMenu` add `w-fit min-w-48` to themselves only when you set no `class`, so setting `class: 'w-full'` also replaces that default. ### One rule for the whole app To make every field fill its column, with a readable maximum, add this to your global CSS: ```css @layer components { /* Every field fills its FormKit wrapper. */ .formkit-inner > [data-slot='root'] { width: 100%; } /* Leaf fields stay readable in a wide form. Containers keep the full width. */ :where(.formkit-outer:not( [data-type='nuxtUIRepeater'], [data-type='nuxtUIEditor'], [data-type='multi-step'], [data-type='step'], [data-type='group'], [data-type='list'] )) { max-width: 32rem; } } /* Selects without their own `class` fill too (see above). */ @layer utilities { .formkit-inner > .w-fit.min-w-48 { width: 100%; } } ``` Why it is written this way: - **The `components` layer** sits below Tailwind's utilities, so a page that sets `w-48`, `w-full`, `flex-1` or `max-w-*` still wins over the default. - **`:where()`** gives the cap zero specificity, so any `max-w-*` overrides it. - **The container exemption** matters: without it the 32rem cap also narrows a repeater's rows or the wizard's tab strip. - **The `utilities` rule** is the one place that has to beat a utility (`w-fit`). It only matches elements that have both `w-fit` and `min-w-48`, which is exactly the select family's own default, so a field with an explicit width is untouched. ::: tip Why this is not built in Whether a field should fill its column or stay compact depends on your layout, so the module does not decide it for you. The same applies to the calendar, which fills its container by default - see [`nuxtUICalendar`](/components/inputs#nuxtuicalendar). ::: ## Colours: use theme tokens Fixed palette classes such as `bg-gray-50 dark:bg-gray-800` look off against the Nuxt UI theme, especially in dark mode. Prefer the semantic tokens, which follow the theme and the colour mode: | Instead of | Use | |---|---| | `border-gray-200 dark:border-gray-700` | `border-default` | | `hover:border-gray-300` | `hover:border-accented` | | `bg-gray-50 dark:bg-gray-800` | `bg-elevated/30` | | `text-gray-400` | `text-muted` | ## Dark mode in scoped styles In a Vue scoped style, `:deep(.dark .my-class)` never matches: `.dark` is on ``, outside the component, so the rule needs a `.dark` ancestor that does not exist inside it. Do not write a separate `.dark` variant. Use a CSS variable that already changes with the mode: ```css :deep(.formkit-repeater-drop-target) { border-color: var(--ui-primary); background-color: color-mix(in oklab, var(--ui-primary) 12%, transparent); } ``` ## Controls that overflow their box Some Nuxt UI controls draw outside the box they occupy. The slider thumb is 16px tall on an 8px track, so it touches the label above and the help text below. Give the field room in CSS: ```css .formkit-outer[data-type='nuxtUISlider'] .formkit-inner { padding-block: 0.5rem; } ``` ## Things beside a field A label sits above its component, so anything placed next to a field in a row (a button, a drag handle, a remove icon) is aligned to the whole field, label included. Use `items-start` on the row and give the extra element a top margin that skips the label (`mt-6`), rather than `items-center`, which sits it too high. ## Repeaters Item layout, button groups, nested repeaters, the drop-target highlight and animation have their own advice: see [Repeater - Styling Best Practices](/components/repeater#styling-best-practices). --- # Generating Forms with AI A form here is a plain JSON array (a [FormKit schema](/getting-started/schema-forms)), 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 1. Paste the [prompt](#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`](https://sfxcode.github.io/nuxt-ui-formkit/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`](https://sfxcode.github.io/nuxt-ui-formkit/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. 2. 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"*. 3. Take the JSON it returns, [render it](#check-the-result) and look at it. 4. Ask for changes in words: *"put the dates side by side"*, *"add a step for payment"*, *"make the phone number optional"*. ## The prompt ```text 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 ``, which adds the Save button itself. ## Node kinds - A field: `{ "$formkit": "", "name": "", "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": ""`. ## 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.` 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 "`, 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`. ``` ## Check the result Never ship generated JSON unseen. Render it: ```vue ``` 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](/getting-started/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.` (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 `||` (second prompt version) | Rendered; the field appeared for *Family* and *Suite* and not for the other rooms | 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): ```json [ { "$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?" } ] ``` ## 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`](https://sfxcode.github.io/nuxt-ui-formkit/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: ```bash mkdir -p .claude/skills cp -r node_modules/@sfxcode/nuxt-ui-formkit/skills/nuxt-ui-formkit .claude/skills/ ``` 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](/getting-started/styling). - Whether a field hidden by `if` keeps 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. --- # Known Limitations Most of these fail **silently** - no error, no warning, just a missing label or a validation message that never appears - so they're worth reading once. ## Setup - **Inputs are not auto-registered.** The module only adds a plugin, the composables, and `FUDataView`/`FUDataEdit`/`FUDataDebug`/`FUAutoForm`. The `nuxtUI*` input and output types exist only once your `formkit.config.ts` registers them via [`createNuxtUiFormkitConfig()`](/getting-started/installation#formkit-configuration), and that file is only picked up if `@formkit/nuxt` is in your `modules`. - **Multi-step needs its plugin.** `nuxtUIMultiStep`/`nuxtUIStep` do nothing unless you call `createNuxtUiFormkitConfig({ multiStep: true })` (or register `createMultiStepPlugin()` yourself). In development a `nuxtUIMultiStep` without the plugin logs a console warning saying so. See [Multi-Step](/components/multi-step). - **Don't override `plugins` after spreading the helper.** `defaultConfig({ ...createNuxtUiFormkitConfig(), plugins: [x] })` replaces the whole array and silently drops the asterisk plugin (required fields lose their `*`). Pass yours as `createNuxtUiFormkitConfig({ plugins: [x] })`, which appends them. - **Schemas can only use globally registered Nuxt UI components.** The module registers `UButton`, `UIcon`, `UTabs`, `UUser` and `UTooltip`. Any other `$cmp: 'UXxx'` in a schema renders as an unstyled custom element. - **Server rendering has a few things that only appear after hydration.** The editor is built in the browser (the content below it moves down by about 73px when it mounts), a multi-step's tab strip appears after hydration, and a date field without an explicit `timeZone` shows the server's day until the page mounts. None of this is a hydration mismatch. See [Server-Side Rendering](/getting-started/ssr). ## Validation timing FormKit reveals validation messages on `blur` by default, and Nuxt UI components don't all emit one. - **`nuxtUIColorPicker` is pointer-only.** Nothing inside `UColorPicker` is focusable, so "blur" means releasing the pointer after picking a color; keyboard users can't operate it at all. - **`nuxtUISelect` reveals on dropdown close only.** Tabbing through without opening it shows nothing. Use `'live'`, or `nuxtUISelectMenu`, which has no such gap. See [Selection Components](/components/inputs#nuxtuiselect). - **`required` passes for a boolean `false`.** On `nuxtUICheckbox`/`nuxtUISwitch`, use the `accepted` rule instead. ## Forms and composables - **`useFormKitForm(id)` is keyed on the form's `id`, not its `name`**, and the form must be mounted in the DOM. `submit()` can't report when FormKit's own submission finishes; `isLoading` only wraps the `handler` you pass. - **`hasErrors` is not `!isValid`.** `hasErrors` counts only error *messages* (e.g. from `setErrors`); a form with failed validation rules reports `hasErrors: false`. Use `isValid` for rule failures. - **`isDirty` is sticky by default** - it stays `true` after a field is edited back to its original value. Set `dirty-behavior="compare"` (`FUDataEdit` has a `dirtyBehavior` prop) to track the live difference. - **Overlays aren't concurrent.** `useFormKitOverlay().edit()`/`auto()` share one overlay instance; a second call before the first resolves takes it over. See [One Overlay Instance at a Time](/components/overlay). - **Autosave attaches at form creation only.** It needs a stable form `name`, covers the root form (not nested groups), and can't be added later from `useFormKitForm`. See [`createAutosavePlugin`](/api/utilities#createautosaveplugin). ## Repeaters and schemas - **`node.at('items.0.email')` can't reach into `nuxtUIRepeater`.** The repeater's outer and inner nodes share a name, so FormKit's address lookup fails silently. The Standard Schema bridge works around this; your own code addressing repeater rows has to as well. - **Inside repeater rows, read sibling values with `$node.value.`.** `$get(name)` resolves by `id`, not by `name`. - **The schema expression language has no unary `!`.** `if: '!$value'` is silently ignored; compare explicitly instead. ## Inputs - **Date inputs emit `@internationalized/date` objects by default** (`CalendarDate` etc.), which aren't JSON-serializable. Set `valueType: 'date'` or `'iso'` for plain values - see [`nuxtUIInputDate`](/components/inputs#nuxtuiinputdate). - **`color` is overridden while a field is invalid** - the wrapped component is switched to its `error` color. - **FormKit's own label/help/messages chrome always surrounds the Nuxt UI component**, so you won't get Nuxt UI's `UFormField` layout. - **`@formkit/addons` plugins that assume native FormKit types silently no-op.** `createMaxLengthCountdownPlugin` and `createFloatingLabelsPlugin` don't work with `nuxtUI*` types. ## FUAutoForm - **Fields it can't map are dropped, not errored.** `null`, `undefined`, functions and other non-plain values never become inputs; with Zod, only string, number, boolean, date, array and object schemas map - enums, unions and literals are skipped. - **Schema introspection is by duck-typing**, tested against Valibot 1.x and Zod 4. Zod 3 isn't supported, and other Standard Schema libraries (e.g. ArkType) get no inference - they work for [validation](/components/standard-schema), not for building the form. ## Localization Built-in labels are English-only unless noted: "Save"/"Cancel"/"Reset" on `FUDataEdit` and the overlays, "Keep editing"/"Discard" in the `confirmDiscard` prompt, "Yes"/"No" on `FUOutputBoolean`, and "Add Item" on the repeater. Multi-step's Previous/Next resolve through FormKit's `$ui.prev`/`$ui.next` i18n keys. Most of these can be overridden through props. ## Not implemented `UStepper` as a multi-step display mode (tabs only), `FUOutputAvatar`, and `FUOutputProgressGroup`. --- # Input Components Comprehensive list of all FormKit-wrapped Nuxt UI input components with schema-based examples. ## Overview Nuxt UI FormKit provides fully-featured input components. All examples below show **schema-based usage** - the recommended approach for building forms. ::: info Schema-First All components are designed to work seamlessly in schemas. Template usage is also supported but schemas are the recommended pattern. ::: ## Text Inputs ### nuxtUIInput Text input component supporting various input types (text, email, password, url, tel, number, search). πŸ“– Nuxt UI reference: [Input](https://ui.nuxt.com/components/input) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIInput', name: 'email', label: 'Email Address', inputType: 'email', placeholder: 'your.email@example.com', leadingIcon: 'i-lucide-mail', validation: 'required|email' } ] ``` **Full Example:** ```vue ``` **Key Props:** - `inputType` - HTML input type (text, email, password, url, tel, search) - `placeholder` - Placeholder text - `leadingIcon` - Icon before input - `trailingIcon` - Icon after input - `size` - Input size (xs, sm, md, lg, xl) ### nuxtUITextarea Multi-line text input with auto-resize functionality. πŸ“– Nuxt UI reference: [Textarea](https://ui.nuxt.com/components/textarea) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUITextarea', name: 'message', label: 'Message', rows: 4, autoresize: true, placeholder: 'Enter your message...', validation: 'required|length:10' } ] ``` ### nuxtUIInputNumber Number input with increment/decrement buttons and formatting options. πŸ“– Nuxt UI reference: [InputNumber](https://ui.nuxt.com/components/input-number) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIInputNumber', name: 'price', label: 'Price', min: 0, step: 0.01, formatOptions: { style: 'currency', currency: 'USD' }, validation: 'required|min:0' } ] ``` **Key Props:** - `locale` - BCP 47 locale used for number formatting (e.g. `'de-DE'`) - `readonly` - Render as read-only ## Selection Components ### nuxtUISelect Dropdown select. It has no search - use [`nuxtUISelectMenu`](#nuxtuiselectmenu) for a searchable select. πŸ“– Nuxt UI reference: [Select](https://ui.nuxt.com/components/select) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUISelect', name: 'country', label: 'Country', options: ['USA', 'UK', 'Canada', 'Australia'], placeholder: 'Select a country', validation: 'required' } ] ``` **With Objects:** ```typescript const schema = [ { $formkit: 'nuxtUISelect', name: 'role', label: 'User Role', options: [ { label: 'Administrator', value: 'admin' }, { label: 'Editor', value: 'editor' }, { label: 'Viewer', value: 'viewer' } ] } ] ``` **Key Props:** - `content` - Positioning/behavior config for the dropdown content **Blur/validation behavior:** `validationVisibility: 'blur'` (FormKit's default) reveals validation messages once the dropdown has been opened and closed at least once β€” Nuxt UI's `USelect` only emits a `blur` event synthetically on that openβ†’close transition, since its trigger has no native blur listener of its own. Tabbing through the trigger without ever opening the dropdown does not trigger this reveal. Set `validationVisibility: 'live'` on the field if messages need to appear without an open/close cycle. ### nuxtUISelectMenu Advanced select with grouping and multiple selection support. πŸ“– Nuxt UI reference: [SelectMenu](https://ui.nuxt.com/components/select-menu) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUISelectMenu', name: 'technologies', label: 'Technologies', multiple: true, options: [ { label: 'Frontend', children: [ { label: 'Vue', value: 'vue' }, { label: 'React', value: 'react' }, { label: 'Svelte', value: 'svelte' } ] }, { label: 'Backend', children: [ { label: 'Node.js', value: 'node' }, { label: 'Python', value: 'python' }, { label: 'Go', value: 'go' } ] } ] } ] ``` **Key Props:** - `size` - Component size - `content` - Positioning/behavior config for the dropdown content - `by` - Key or comparator used to match the selected value **Blur/validation behavior:** `validationVisibility: 'blur'` (FormKit's default) reveals validation messages both when the dropdown is opened and closed, and when the trigger is simply focused and blurred without ever opening it β€” a real DOM blur listener is wired on the underlying trigger either way. ### nuxtUIListbox Listbox for single/multiple selection with filtering, plus a two-pane "transfer" mode for moving items between an available and a selected list. πŸ“– Nuxt UI reference: [Listbox](https://ui.nuxt.com/components/listbox) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIListbox', name: 'skills', label: 'Skills', multiple: true, options: ['TypeScript', 'Vue', 'Nuxt', 'FormKit', 'TailwindCSS'] } ] ``` **Key Props:** - `options` - Array of strings or `{ label, value, description, icon, avatar, chip, disabled }` objects (or nested arrays of these for grouped/labeled sections) - `multiple` - Allow selecting more than one item - `filter` - Show a search input to filter items (`true`, or an object of `UInput` props) - `by` / `valueKey` - Key or comparator used to match the selected value against `options` - `color` - Component color **With Transfer Mode:** Set `displayMode: 'transfer'` to render a two-pane UI instead: an "available" list on the left and a "selected" list on the right, with `v-model` bound to the array of items on the right. ```typescript const schema = [ { $formkit: 'nuxtUIListbox', name: 'teamMembers', label: 'Project Team', displayMode: 'transfer', options: employees, filter: true, transferLeftHeaderText: 'Available', transferRightHeaderText: 'Selected', transferAll: true, transferSortIcons: true, transferItemDraggable: true } ] ``` Items can be moved between the two panes either with the arrow buttons or by dragging β€” in **either direction** β€” and reordered within the "selected" pane by dragging or with the optional up/down icons. A precise insertion-line indicator (not a whole-row highlight) shows exactly where a dragged item will land. **Transfer Mode Props:** - `displayMode` - `'single'` (default) or `'transfer'` - `transferLeftHeaderText` / `transferRightHeaderText` - Optional header labels above each pane - `transferHeaderClass` - CSS classes applied to both header labels (defaults to `'text-sm font-medium text-highlighted'`) - `transferAll` - Show "transfer all" / "remove all" buttons (the double-chevron buttons) in addition to the single-item transfer buttons - `transferSortIcons` - Show up/down chevron buttons on each item in the "selected" pane for reordering without dragging (off by default) - `transferItemDraggable` - Make the whole row draggable, not just its grip handle (off by default β€” the grip handle is always draggable) ::: tip All the single-mode props above (`options`, `multiple`, `filter`, `by`/`valueKey`, `orientation`, `virtualize`, etc.) are also forwarded to both panes in transfer mode, except `color`, which currently only applies to single-select mode. ::: ### nuxtUIInputMenu Dropdown menu with searchable options. πŸ“– Nuxt UI reference: [InputMenu](https://ui.nuxt.com/components/input-menu) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIInputMenu', name: 'framework', label: 'Framework', options: ['Nuxt', 'Next.js', 'SvelteKit', 'Remix', 'Astro'], placeholder: 'Search frameworks...' } ] ``` **Key Props:** - `mode` - `'combobox'` (default) or `'autocomplete'` - `content` - Positioning/behavior config for the dropdown content - `by` - Key or comparator used to match the selected value - `open` - Controlled open state of the menu ### nuxtUITree Hierarchical selection input for nested data β€” categories, org charts, file trees, permission trees. πŸ“– Nuxt UI reference: [Tree](https://ui.nuxt.com/components/tree) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUITree', name: 'category', label: 'Category', options: [ { label: 'Electronics', icon: 'i-lucide-cpu', defaultExpanded: true, children: [ { label: 'Laptops' }, { label: 'Phones' }, ], }, { label: 'Clothing', icon: 'i-lucide-shirt', children: [ { label: 'Men' }, { label: 'Women' }, ], }, ], }, ] ``` ::: info The field's value is the **selected item only** β€” whatever shape you gave that item in `options` (e.g. `{ label: 'Laptops' }` above). Expanding/collapsing nodes is separate, uncontrolled UI state that never becomes part of the field's value, the same way `FUSelectMenu`'s `open`/`defaultOpen` are plain display props rather than part of `modelValue`. Selecting a node and toggling its expanded state are also independent interactions β€” clicking a row does both at once, but keyboard `ArrowLeft`/`ArrowRight` toggles expand without changing the selection. ::: **Key Props:** - `options` - Array of `{ label, icon, children, defaultExpanded, disabled, ... }` objects; `children` nests recursively to any depth - `multiple` - Allow selecting more than one item (the value becomes an array) - `labelKey` - Key used to read each item's label (defaults to `'label'`) - `nested` - Render children inside their parent (`true`, default) vs. a flattened single-level list - `virtualize` - Enable virtualization for large trees (`true`, or `{ overscan, estimateSize }`) - `expandedIcon` / `collapsedIcon` / `trailingIcon` - Icons shown next to expandable parent nodes - `selectionBehavior` - `'toggle'` or `'replace'` selection behavior ## Boolean Inputs ### nuxtUICheckbox Single checkbox with label and description. πŸ“– Nuxt UI reference: [Checkbox](https://ui.nuxt.com/components/checkbox) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUICheckbox', name: 'terms', label: 'I agree to the terms and conditions', description: 'You must accept to continue', validation: 'accepted' } ] ``` **Key Props:** - `trueValue` / `falseValue` - Custom values emitted for the checked / unchecked states ### nuxtUICheckboxGroup Multiple checkbox selection. πŸ“– Nuxt UI reference: [CheckboxGroup](https://ui.nuxt.com/components/checkbox-group) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUICheckboxGroup', name: 'interests', label: 'Interests', options: [ { label: 'Technology', value: 'tech', description: 'Software and hardware' }, { label: 'Design', value: 'design', description: 'UI/UX and graphics' }, { label: 'Business', value: 'business', description: 'Strategy and growth' } ] } ] ``` **Key Props:** - `name` - Shared form control name for the group ### nuxtUIRadioGroup Radio button group for single selection. πŸ“– Nuxt UI reference: [RadioGroup](https://ui.nuxt.com/components/radio-group) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIRadioGroup', name: 'plan', label: 'Select Plan', options: [ { label: 'Free', value: 'free', description: 'Basic features for getting started' }, { label: 'Pro', value: 'pro', description: 'Advanced features for professionals' }, { label: 'Enterprise', value: 'enterprise', description: 'Full features with dedicated support' } ], validation: 'required' } ] ``` ### nuxtUISwitch Toggle switch for boolean values. πŸ“– Nuxt UI reference: [Switch](https://ui.nuxt.com/components/switch) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUISwitch', name: 'newsletter', label: 'Subscribe to newsletter', help: 'Get product updates and announcements' } ] ``` **Key Props:** - `trueValue` / `falseValue` - Custom values emitted for the on / off states ## Specialized Inputs ### nuxtUIInputDate Date and time picker with range support. πŸ“– Nuxt UI reference: [InputDate](https://ui.nuxt.com/components/input-date) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIInputDate', name: 'eventDate', label: 'Event Date', enableTimePicker: false, placeholder: 'Select date', validation: 'required' } ] ``` **With Time:** ```typescript const schema = [ { $formkit: 'nuxtUIInputDate', name: 'appointment', label: 'Appointment', enableTimePicker: true, placeholder: 'Select date and time' } ] ``` **Working with JS `Date` or ISO strings:** By default the form value is a native `@internationalized/date` `CalendarDate` / `CalendarDateTime` / `ZonedDateTime` β€” the type `UInputDate` itself uses. If your form data instead holds a JS `Date` or an ISO 8601 string, set `valueType` to convert at the boundary in both directions. `defaultValue`, `placeholder`, `defaultPlaceholder`, `minValue` and `maxValue` accept the same types and are converted the same way. ```typescript const schema = [ { $formkit: 'nuxtUIInputDate', name: 'eventDate', label: 'Event Date', valueType: 'date', // 'calendar' (default) | 'date' | 'iso' defaultValue: new Date(2026, 6, 5) }, { $formkit: 'nuxtUIInputDate', name: 'isoDate', label: 'ISO Date', valueType: 'iso', defaultValue: '2026-07-05' } ] ``` **Key Props:** - `valueType` - `'calendar'` (default), `'date'`, or `'iso'` β€” the shape of the value read from and written to the form - `timeZone` - Timezone used when converting to/from `'date'`/`'iso'`; defaults to the local timezone ### nuxtUIInputTime Time picker with 12/24-hour format. πŸ“– Nuxt UI reference: [InputTime](https://ui.nuxt.com/components/input-time) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIInputTime', name: 'meetingTime', label: 'Meeting Time', placeholder: 'Select time' } ] ``` ### nuxtUIColorPicker Color selection with multiple format support (`hex`, `rgb`, `hsl`, `cmyk`, `lab`). πŸ“– Nuxt UI reference: [ColorPicker](https://ui.nuxt.com/components/color-picker) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIColorPicker', name: 'themeColor', label: 'Theme Color', format: 'hex' } ] ``` ::: info Blur/validation behavior `UColorPicker` is pointer-only - nothing inside it is focusable - so there is no real blur. Instead, the end of a pointer interaction on the picker (releasing the mouse after picking a color) counts as the field being touched, and FormKit's default `validationVisibility: 'blur'` reveals validation messages at that moment. Keyboard users can't operate the picker at all (a Nuxt UI limitation). ::: ### nuxtUIInputTags Tag input component with a custom delimiter. πŸ“– Nuxt UI reference: [InputTags](https://ui.nuxt.com/components/input-tags) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIInputTags', name: 'tags', label: 'Tags', placeholder: 'Add tags...', delimiter: ',', help: 'Press comma to add a tag' } ] ``` **Key Props:** - `delimiter` - Character or `RegExp` used to split tags (e.g. `','`) ### nuxtUIPinInput PIN/OTP entry component. πŸ“– Nuxt UI reference: [PinInput](https://ui.nuxt.com/components/pin-input) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIPinInput', name: 'pin', label: 'Enter PIN', length: 6, pinType: 'number', validation: 'required|length:6' } ] ``` **Key Props:** - `pinType` - `'text'` (default) or `'number'` - `separator` - Insert a separator between inputs (index or list of indexes) ### nuxtUIInputRating Star-based rating input for numeric values. πŸ“– Nuxt UI reference: [InputRating](https://ui.nuxt.com/components/input-rating) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIInputRating', name: 'satisfaction', label: 'Satisfaction', length: 5, validation: 'required' } ] ``` **Key Props:** - `length` - Number of rating items to render (default `5`) - `step` - Granularity of each rating item (`1`, `0.5`, `0.25`, or `0.1`) - `icon` / `emptyIcon` - Custom icons for filled/empty states - `clearable` - Clicking the current value resets it to `0` - `hoverable` - Preview the value under the pointer on hover - `readonly` - Non-interactive display mode ### nuxtUISlider Range slider for numeric values. πŸ“– Nuxt UI reference: [Slider](https://ui.nuxt.com/components/slider) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUISlider', name: 'volume', label: 'Volume', min: 0, max: 100, step: 1 } ] ``` ::: tip The slider's thumb is taller than its track and overflows it, so it touches the label above and the help text below. Give the track vertical room with CSS: `.formkit-outer[data-type='nuxtUISlider'] .formkit-inner { padding-block: 0.5rem; }`. ::: ### nuxtUICalendar Bare date-grid picker with day, month, and year views, supporting range and multiple selection. πŸ“– Nuxt UI reference: [Calendar](https://ui.nuxt.com/components/calendar) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUICalendar', name: 'appointmentDate', label: 'Appointment Date', validation: 'required' } ] ``` **With Range:** ```typescript const schema = [ { $formkit: 'nuxtUICalendar', name: 'vacationRange', label: 'Vacation Dates', range: true } ] ``` **Working with JS `Date` or ISO strings:** By default the form value is a native `@internationalized/date` `DateValue` (or `{ start, end }` / an array of those for range/multiple mode) β€” the type `UCalendar` itself uses. Set `valueType` to convert to/from a JS `Date` or an ISO 8601 string at the boundary, in both directions. Works the same for single, range, and multiple selection. ```typescript const schema = [ { $formkit: 'nuxtUICalendar', name: 'appointmentDate', label: 'Appointment Date', valueType: 'date', // 'calendar' (default) | 'date' | 'iso' defaultValue: new Date(2026, 6, 5) } ] ``` **Key Props:** - `calendarType` - `'date'` (default), `'month'`, or `'year'` picker view - `range` - Enable selecting a start/end date range - `multiple` - Enable selecting multiple individual dates - `color` - Theme color for the selected date(s) - `size` - `'xs' | 'sm' | 'md' | 'lg' | 'xl'` - controls cell/font size, not the overall footprint - `valueType` - `'calendar'` (default), `'date'`, or `'iso'` β€” the shape of the value read from and written to the form - `timeZone` - Timezone used when converting to/from `'date'`/`'iso'`; defaults to the local timezone ::: tip The calendar grid fills its container by default (matching Nuxt UI's own `UCalendar`). Add a width class via `class` on the schema entry (e.g. `class: 'w-1/2'`) to control how much space it takes up. ::: ### nuxtUIFileUpload Drag/drop and click-to-browse file input with previews. πŸ“– Nuxt UI reference: [FileUpload](https://ui.nuxt.com/components/file-upload) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIFileUpload', name: 'attachment', label: 'Attachment', accept: 'image/*', validation: 'required' } ] ``` **With Multiple Files:** ```typescript const schema = [ { $formkit: 'nuxtUIFileUpload', name: 'documents', label: 'Documents', multiple: true, layout: 'grid' } ] ``` **Key Props:** - `multiple` - Allow selecting more than one file - `accept` - Comma-separated MIME types or extensions (e.g. `'image/png,.pdf'`) - `layout` - `'list'` (default) or `'grid'` β€” only applies when `variant` is `'area'` - `position` - `'outside'` (default) or `'inside'` β€” file list position relative to the dropzone - `variant` - `'area'` (default) or `'button'` β€” `'button'` only works when `multiple` is `false` ### nuxtUIEditor Tiptap-based rich text editor with a built-in formatting toolbar. πŸ“– Nuxt UI reference: [Editor](https://ui.nuxt.com/components/editor) **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIEditor', name: 'body', label: 'Post Body', contentType: 'html', validation: 'required' } ] ``` **With Markdown:** ```typescript const schema = [ { $formkit: 'nuxtUIEditor', name: 'notes', label: 'Notes', contentType: 'markdown' } ] ``` **With Custom Toolbar Items:** ```typescript const schema = [ { $formkit: 'nuxtUIEditor', name: 'summary', label: 'Summary', toolbarItems: [ [{ kind: 'undo', icon: 'i-lucide-undo' }, { kind: 'redo', icon: 'i-lucide-redo' }], [{ kind: 'mark', mark: 'bold', icon: 'i-lucide-bold' }, { kind: 'mark', mark: 'italic', icon: 'i-lucide-italic' }] ] } ] ``` **Key Props:** - `contentType` - `'html'`, `'markdown'`, or `'json'` β€” auto-inferred from the value when unset - `placeholder` - Placeholder text shown in empty paragraphs, or an object with a `mode: 'firstLine' | 'everyLine'` - `markdown` - Options for markdown parsing/serialization - `image` - Enable/configure the image extension (`false` to disable) - `mention` - Enable/configure the mention extension (`false` to disable) - `extensions` - Array of additional Tiptap extensions (e.g. `Underline`, `Emoji`) to register alongside the defaults - `toolbar` - Set to `false` to hide the built-in formatting toolbar entirely - `toolbarItems` - Override the default toolbar item groups (undo/redo, headings, marks, lists/blocks) β€” see [Nuxt UI's `EditorToolbarItem`](https://ui.nuxt.com/components/editor#toolbar) for the full `kind` list - `editorHandlers` - Custom handler overrides for toolbar item behavior (renamed from Nuxt UI's `handlers` prop to avoid colliding with FormKit's own `context.handlers`) ::: warning Unlike every other input, `nuxtUIEditor` has no `color`, `variant`, `size`, or `disabled` prop β€” it's a Tiptap options wrapper, not a themed Nuxt UI form control. FormKit's disabled state is mapped to Tiptap's `editable` option instead (the editor becomes read-only rather than visually "disabled"). ::: ## Complete Form Example Here's a comprehensive example using multiple input types: ```vue ``` ## Template Usage (Alternative) While schemas are recommended, template-based usage is also supported: ```vue ``` ::: info Recommendation Use schemas for better maintainability and type safety. Use templates only for simple forms or when you need fine-grained layout control. ::: ## Common Props Every input supports FormKit's own fields: - `name` (required) - Field name for form data - `label` - Field label text - `help` - Help text below field - `validation` - Validation rules - `disabled` - Disable the input Props like `placeholder` and `size` are forwarded to the wrapped Nuxt UI component, so they only apply where that component has them (a checkbox or switch has no placeholder). Check each component's section above. ## Next Steps ### πŸ‘οΈ Output Components Display-only components with schemas [View Output Components β†’](/components/outputs) ### πŸ”„ Repeater Dynamic repeatable sections in schemas [Learn About Repeaters β†’](/components/repeater) ### πŸ’‘ Examples Real-world schema-based forms [Browse Examples β†’](/examples/) --- # Output Components ## Overview Output components render data in read-only mode with beautiful formatting. Perfect for dashboards, profile pages, and data visualization - all fully integrated with schemas. ::: info **Schema-First:** Output components work seamlessly with `FUDataView` for creating display-only forms from schemas. ::: ## Text Outputs ### nuxtUIOutputText Display text with style customization and icons. **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIOutputText', name: 'userName', label: 'Username', leadingIcon: 'i-lucide-user', color: 'primary', size: 'lg' } ] ``` **Complete Example:** ```vue ``` **Key Props:** - `leadingIcon` - Icon before text - `trailingIcon` - Icon after text - `color` - Text color - `size` - Text size - `outputType` - How the value is rendered (see below) **`outputType` values:** | Value | Description | | --- | --- | | `text` (default) | Render the raw value as plain text | | `email` | Render as a `mailto:` link | | `url` | Render as an external link (opens in a new tab) | | `tel` | Render as a `tel:` link | | `color` | Render the value as a hex color swatch | | `duration` | Format the value as a human-readable duration (e.g. `1h 30m`) | | `i18n` | Translate the value using it as the i18n message key | **i18n Example:** The `i18n` type uses the field value as the translation key via `vue-i18n`'s `useI18n()`. With a value of `status.active` and a matching message, it renders the translated text; when no i18n instance is active it falls back to the raw value. ```typescript const schema = [ { $formkit: 'nuxtUIOutputText', name: 'status', label: 'Status', outputType: 'i18n' // value "status.active" β†’ "Active" } ] ``` > Requires `vue-i18n` (or `@nuxtjs/i18n`) to be installed and configured in your app. It is an optional peer dependency β€” without it, the value is shown unchanged. ### nuxtUIOutputLink Display URLs with automatic link generation. **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIOutputLink', name: 'website', label: 'Website', leadingIcon: 'i-lucide-link', target: '_blank' }, { $formkit: 'nuxtUIOutputLink', name: 'email', label: 'Email', leadingIcon: 'i-lucide-mail', target: '_blank' } ] ``` **Key Props:** - `target` - Link target (_blank, _self) - `external` - Mark as external link - `leadingIcon` - Icon before link ## Formatted Outputs ### nuxtUIOutputNumber Display numbers with advanced formatting (currency, percentage, decimal). **Schema Example:** ```typescript const schema = [ // Currency { $formkit: 'nuxtUIOutputNumber', name: 'price', label: 'Price', formatOptions: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 } }, // Percentage { $formkit: 'nuxtUIOutputNumber', name: 'growth', label: 'Growth Rate', formatOptions: { style: 'percent', minimumFractionDigits: 1 } }, // Decimal { $formkit: 'nuxtUIOutputNumber', name: 'rating', label: 'Rating', formatOptions: { minimumFractionDigits: 1, maximumFractionDigits: 2 } } ] ``` **Complete Dashboard Example:** ```vue ``` ### nuxtUIOutputProgress Display a numeric value as a progress bar. **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIOutputProgress', name: 'storageUsed', label: 'Storage Used', max: 100, status: true, color: 'primary' }, { $formkit: 'nuxtUIOutputProgress', name: 'quotaUsed', label: 'Quota', max: 500, status: true, color: 'warning' } ] ``` **Notes:** - The field's value is the current progress amount, not a percentage - it's read against `max` (defaults to `100`). - `status: true` overlays the computed percentage as text; omit it for a bare bar. - `orientation: 'vertical'` renders a vertical bar instead of the default horizontal one. ### nuxtUIOutputRating Display a numeric rating as a read-only row of stars. **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIOutputRating', name: 'reviewScore', label: 'Review Score', length: 5, color: 'warning' }, { $formkit: 'nuxtUIOutputRating', name: 'satisfaction', label: 'Satisfaction', length: 10, size: 'sm', orientation: 'horizontal' } ] ``` **Notes:** - The field's value is the current rating; `length` (default `5`) sets the number of stars, not `max`. - This component is always read-only - it wraps the same star widget as `nuxtUIInputRating`, but forces Nuxt UI's `readonly` mode so clicking a star never changes the value. - `icon`/`emptyIcon` customize the filled/empty star glyphs; `color`/`size` follow the same values as every other Output component. ### nuxtUIOutputDate Display dates and times with customizable formatting. **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIOutputDate', name: 'createdAt', label: 'Created', format: 'PPP' // October 14, 2023 }, { $formkit: 'nuxtUIOutputDate', name: 'updatedAt', label: 'Last Updated', format: 'Pp', // Oct 14, 2023, 3:30 PM }, { $formkit: 'nuxtUIOutputDate', name: 'lastLogin', label: 'Last Login', relative: true // "2 hours ago" } ] ``` **Format Options:** - `PP` - Oct 14, 2023 - `PPP` - October 14, 2023 - `PPPP` - Saturday, October 14, 2023 - `Pp` - Oct 14, 2023, 3:30 PM - `relative: true` - Relative time (e.g., "2 hours ago") ### nuxtUIOutputBoolean Display boolean values with custom text and icons. **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIOutputBoolean', name: 'isActive', label: 'Status', trueValue: 'Active', falseValue: 'Inactive', trueIcon: 'i-lucide-check-circle', falseIcon: 'i-lucide-x-circle', trueColor: 'green', falseColor: 'red' }, { $formkit: 'nuxtUIOutputBoolean', name: 'isPremium', label: 'Account Type', trueValue: 'Premium', falseValue: 'Free', trueIcon: 'i-lucide-star', falseIcon: 'i-lucide-user' } ] ``` ## Collection Outputs ### nuxtUIOutputList Display arrays/lists with various styles. **Schema Example:** ```typescript const schema = [ // Badge style { $formkit: 'nuxtUIOutputList', name: 'skills', label: 'Skills', listType: 'badge', color: 'primary' }, // Bullet list { $formkit: 'nuxtUIOutputList', name: 'features', label: 'Features', listType: 'ul' }, // Numbered list { $formkit: 'nuxtUIOutputList', name: 'steps', label: 'Steps', listType: 'ol' }, // Comma-separated { $formkit: 'nuxtUIOutputList', name: 'categories', label: 'Categories', listType: 'inline', separator: ' β€’ ' } ] ``` **Complete Example:** ```vue ``` ## Status Outputs ### nuxtUIOutputBadge Display a single value as a status badge. ::: tip Not to be confused with `nuxtUIOutputList`'s `listType: 'badge'` mode just above - that renders an **array** as multiple badges (e.g. a list of tags). `nuxtUIOutputBadge` is for a **single** status value (e.g. `"Active"`, `"Pending"`). ::: **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIOutputBadge', name: 'status', label: 'Status', color: 'success', variant: 'subtle' }, { $formkit: 'nuxtUIOutputBadge', name: 'priority', label: 'Priority', color: 'error', variant: 'solid', badgeIcon: 'i-heroicons-exclamation-triangle' } ] ``` **Notes:** - `badgeIcon` renders an icon *inside* the badge pill - distinct from the common `leadingIcon`/`trailingIcon`, which render an external icon beside it. - `variant` accepts this library's usual `outline | soft | subtle | ghost | none`, but the underlying `UBadge` only supports `outline | soft | subtle | solid` - `ghost` and `none` both map to `soft`. ## Profile Outputs ### nuxtUIOutputUser Display a name, description, and avatar together as a profile-style entry. ::: warning Unlike every other Output component, this field's **value is an object**, not a scalar: `{ name?, description?, avatar? }`. `avatar` mirrors Nuxt UI's own `Avatar` props (`src`, `alt`, `icon`, `text`). ::: **Schema Example:** ```typescript const schema = [ { $formkit: 'nuxtUIOutputUser', name: 'author', label: 'Author', orientation: 'horizontal' }, { $formkit: 'nuxtUIOutputUser', name: 'assignee', label: 'Assigned To', chip: true } ] const data = { author: { name: 'Jane Doe', description: 'Senior Engineer', avatar: { src: 'https://example.com/jane.png' } }, assignee: { name: 'John Smith', description: 'Online now', avatar: { text: 'JS' } } } ``` **Notes:** - `avatar.src` shows an image; omit it and `avatar.text`/`avatar.icon` render initials or an icon instead. - `chip: true` overlays a status indicator (e.g. online/offline) on the avatar, using Nuxt UI's default chip styling; pass a `Chip` props object instead of `true` to customize it. - `orientation: 'vertical'` stacks the avatar above the name/description instead of side-by-side. ## Complete User Profile Example Here's a comprehensive example combining all output types: ```vue ``` ## Common Props All output components support: - `name` (required) - Field name in data object - `label` - Display label - `help` - Help text below value - `color` - Component color - `size` - Component size ### Icon Tooltips Every `icon`/`leadingIcon`/`trailingIcon` pair described above also accepts a matching `*Tooltip` prop - `iconTooltip`, `leadingIconTooltip`, `trailingIconTooltip` (plus `trueIconTooltip`/`falseIconTooltip` on `nuxtUIOutputBoolean`, matching its `trueIcon`/`falseIcon`). Each accepts any Nuxt UI [`Tooltip`](https://ui.nuxt.com/components/tooltip) prop except `class` (e.g. `text`, `content`, `arrow`, `delayDuration`) as a plain object. No tooltip is rendered unless one is supplied. ```typescript const schema = [ { $formkit: 'nuxtUIOutputText', name: 'userName', label: 'Username', leadingIcon: 'i-lucide-user', leadingIconTooltip: { text: 'Unique account identifier' } } ] ``` ### Icon Accessibility An icon with a click handler (`onIconClicked`, `onLeadingIconClicked`, `onTrailingIconClicked`) renders as a real `