---
title: "Customization"
description: "Field options, custom inputs, field slots and the actions slot."
canonical_url: "https://nuxt-formisch.onmax.me/auto-form/customization"
---
# Customization

> Field options, custom inputs, field slots and the actions slot.

Field options set in the schema with `v.metadata()` travel with the schema. `field-config` overrides them for one form, custom components replace a single input, and slots replace the markup.

## field-config

Keys are dotted paths without array indexes: `email`, `address.city`, `members.name`. Options set here win over the schema's metadata.

```vue
<script setup lang="ts">
const fieldConfig = {
  'email': { label: 'Work email', placeholder: 'you@company.com' },
  'role': { section: 'Access', order: 1, disabled: !isAdmin.value },
  'internalNotes': { hidden: true },
  'members.name': { placeholder: 'Full name' },
} satisfies Record<string, FieldConfig>
</script>

<template>
  <FAutoForm :schema="UserSchema" :field-config="fieldConfig" @submit="save" />
</template>
```

## Field options

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      In <code>
        v.metadata()
      </code>
    </th>
    
    <th>
      
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        label
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      yes (or <code>
        v.title()
      </code>
      
      )
    </td>
    
    <td>
      Label text.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        description
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      yes (or <code>
        v.description()
      </code>
      
      )
    </td>
    
    <td>
      Help text.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        placeholder
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      yes
    </td>
    
    <td>
      Placeholder.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unit
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      yes
    </td>
    
    <td>
      Text at the end of the input, such as <code>
        kg
      </code>
      
       or <code>
        %
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        section
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      yes
    </td>
    
    <td>
      Groups top-level fields under a heading.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        order
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      yes
    </td>
    
    <td>
      Sort order among siblings. Lower first; unset counts as <code>
        0
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        hidden
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      yes
    </td>
    
    <td>
      Leaves the field out.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        colSpan
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      yes
    </td>
    
    <td>
      Grid columns to span.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        disabled
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      yes
    </td>
    
    <td>
      Disables the input.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        type
      </code>
    </td>
    
    <td>
      <code>
        'textarea' | 'json'
      </code>
    </td>
    
    <td>
      yes
    </td>
    
    <td>
      Renders a string as a <code>
        UTextarea
      </code>
      
      , or any value as editable JSON.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        component
      </code>
    </td>
    
    <td>
      <code>
        Component
      </code>
    </td>
    
    <td>
      no
    </td>
    
    <td>
      Replaces the input.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        componentProps
      </code>
    </td>
    
    <td>
      <code>
        Record<string, unknown>
      </code>
    </td>
    
    <td>
      no
    </td>
    
    <td>
      Extra props for <code>
        component
      </code>
      
      .
    </td>
  </tr>
</tbody>
</table>

`FieldConfig` and `ResolvedField` are auto-imported types.

## Custom inputs

A custom component replaces the input inside the field's `UFormField`, so the label, help text and error stay. It gets `modelValue` and `disabled`, plus `componentProps`, and emits `update:modelValue`:

```vue [app/components/ColorSwatches.vue]
<script setup lang="ts">
defineProps<{ swatches: string[], disabled?: boolean }>()
const color = defineModel<string>()
</script>

<template>
  <div class="flex gap-2">
    <button
      v-for="swatch in swatches"
      :key="swatch"
      type="button"
      class="size-8 rounded-md"
      :class="{ 'ring-2 ring-primary': color === swatch }"
      :style="{ backgroundColor: swatch }"
      :disabled="disabled"
      @click="color = swatch"
    />
  </div>
</template>
```

```vue
<script setup lang="ts">
import ColorSwatches from '~/components/ColorSwatches.vue'

const fieldConfig = {
  accent: { component: ColorSwatches, componentProps: { swatches: ['#10b981', '#3b82f6', '#f43f5e'] } },
}
</script>
```

## Field slots

A `field:<dotted.key>` slot replaces a field, label and all. It gets the resolved `field`, its Formisch `path`, and the `form`, so `<FField>` connects your markup:

```vue
<FAutoForm :schema="PostSchema" @submit="publish">
  <template #[`field:body`]="{ field, path, form }">
    <FField v-slot="body" :of="form" :path="path">
      <UFormField :label="field.label" :error="body.errors?.[0]">
        <UTextarea v-model="body.input" :rows="8" autoresize class="w-full" />
      </UFormField>
    </FField>
  </template>
</FAutoForm>
```

Slot names with dots need the dynamic form, `#[`field:address.city`]`, because Vue reads `#field:address.city` as a slot called `field:address` with a `.city` modifier. The dynamic form works for every key, so use it everywhere.

For array items the key leaves the index out (`field:members.name`), and `path` has the index in it (`['members', 2, 'name']`).

## Actions

The `actions` slot replaces the submit button. It gets `form` and `reset`:

```vue
<FAutoForm :schema="PostSchema" @submit="publish">
  <template #actions="{ form, reset }">
    <UButton type="submit" :loading="form.isSubmitting">Publish</UButton>
    <UButton variant="ghost" :disabled="!form.isDirty" @click="reset">Reset</UButton>
  </template>
</FAutoForm>
```

## introspectSchema

`<FAutoForm>` builds its field tree with `introspectSchema(schema, fieldConfig?)`, auto-imported with AutoForm. Use it to render a schema your own way:

```ts
const fields = introspectSchema(v.object({ email: v.pipe(v.string(), v.email()) }))
// [{ key: 'email', name: 'email', type: 'string', label: 'Email', required: true, constraints: { format: 'email' } }]
```

Each `ResolvedField` has `key`, `name`, `type`, `label`, `required` and `constraints`, the field options above, `options` for enums, `fields` for objects and `item` for arrays. Hidden fields are dropped and siblings sorted by `order`.


## Sitemap

See the full [sitemap](https://nuxt-formisch.onmax.me/sitemap.md) for all pages.
