---
title: "Slots"
description: "Customize AutoForm's header/footer, field controls, before/after field and layout content via named slots."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/auto-form/slots"
---
# Slots

> Customize AutoForm's header/footer, field controls, before/after field and layout content via named slots.

## Overview

The `AutoForm` slot system is organized into three levels:

- **Form slots** - Customize the overall form structure (`header`, `footer`, `submit`)
- **Field slots** - Fine-tune individual parts of each field (`field-label`, `field-hint`, `field-error`, etc.)
- **Content slots** - Take over the rendering of object and array fields, or insert content before/after fields (`field-content`, `field-before`, `field-after`)

> [!TIP]
> 
> Field slots support both the 
> 
> generic pattern
> 
> field-{type}
> 
>  and the 
> 
> named pattern
> 
> field-{type}:{path}
> 
> , allowing you to unify styles while precisely controlling specific fields. Named patterns take priority over generic patterns.

## Examples

### Form Level

`header`, `footer`, and `submit` take over the form's head, footer and submit area — all can read `loading`, `errors`, `state` to customize the outer layer.

> [!WARNING]
> 
> When using the 
> 
> submit
> 
>  slot, set 
> 
> :submit="false"
> 
>  to disable the default submit button, otherwise it will be rendered twice.

```vue [AutoFormSlotsFormExample.vue]
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  email: afz.email().meta({ label: '邮箱' }),
  password: afz.string({ type: 'withPasswordToggle' }).min(6).meta({ label: '密码' }),
  remember: afz.boolean({ type: 'switch', controlProps: { label: '记住登录状态' } }).default(true).meta({ label: '记住我' })
})

const form = reactive<Partial<z.output<typeof schema>>>({})

async function onSubmit() {
  await new Promise(resolve => setTimeout(resolve, 1200))
}
</script>

<template>
  <MAutoForm
    :schema="schema"
    :state="form"
    :submit="false"
    @submit="onSubmit"
  >
    <template #header="{ loading }">
      <UAlert
        color="info"
        variant="soft"
        icon="i-lucide-log-in"
        title="header 状态展示"
        description="header 在字段渲染前接收 loading，可同步说明与提交状态"
      >
        <template v-if="loading" #actions>
          <UBadge color="info" variant="subtle">
            提交中
          </UBadge>
        </template>
      </UAlert>
    </template>

    <template #footer="{ errors, state }">
      <div class="mt-3 flex flex-wrap items-center gap-2 text-sm">
        <UBadge :color="errors.length ? 'error' : 'success'" variant="subtle">
          {{ errors.length ? `${errors.length} 个错误` : '校验通过' }}
        </UBadge>
        <UBadge color="neutral" variant="subtle">
          {{ state.remember ? '保持登录' : '本次登录' }}
        </UBadge>
      </div>
    </template>

    <template #submit="{ errors, loading }">
      <div class="mt-4 flex gap-2">
        <UButton type="submit" icon="i-lucide-send" :loading="loading" :disabled="errors.length > 0">
          登录
        </UButton>
        <UButton variant="outline" color="neutral" :disabled="loading">
          创建账号
        </UButton>
      </div>
    </template>
  </MAutoForm>
</template>
```

### Generic Field

`field-label`, `field-description`, `field-hint`, `field-error` uniformly override the label, description, hint and error areas of all fields.

```vue [AutoFormSlotsGenericExample.vue]
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  username: afz.string().min(3).meta({ label: '用户名', hint: '至少 3 个字符' }),
  email: afz.email().meta({ label: '邮箱', description: '用于接收通知' }),
  role: afz.enum(['admin', 'user', 'guest']).meta({ label: '角色' }),
  active: afz.boolean({ controlProps: { label: '启用账户' } }).default(true).meta({ label: '状态' })
})

const state = reactive<Partial<z.output<typeof schema>>>({})
</script>

<template>
  <MAutoForm :schema="schema" :state="state" :validate-on="['input', 'blur']">
    <template #field-label="{ label, path }">
      <span class="inline-flex items-center gap-2">
        <UBadge color="primary" variant="subtle" size="xs">
          {{ label }}
        </UBadge>
        <span class="text-xs text-muted">
          {{ path }}
        </span>
      </span>
    </template>

    <template #field-description="{ description }">
      <span
        v-if="description"
        class="text-xs text-toned"
      >
        {{ description }}
      </span>
    </template>

    <template #field-hint="{ hint }">
      <span class="inline-flex items-center gap-1 text-xs text-muted">
        <UIcon name="i-lucide-info" class="size-3" />
        <span>{{ hint || '通用 hint slot' }}</span>
      </span>
    </template>

    <template #field-error="{ error }">
      <UAlert
        v-if="error"
        color="error"
        variant="subtle"
        icon="i-lucide-triangle-alert"
        :description="String(error)"
        class="mt-2"
      />
    </template>
  </MAutoForm>
</template>
```

### Named Field

`field-{type}:{path}` replaces only the specified area of a given field; unmatched fields keep their default rendering. `field-default:{path}` completely replaces the control for that field.

> [!NOTE]
> 
> Named slot priority is higher than generic slots: when both 
> 
> field-label
> 
>  and 
> 
> field-label:username
> 
>  exist, the 
> 
> username
> 
>  field uses the latter.

```vue [AutoFormSlotsKeyedExample.vue]
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  username: afz.string().min(2).meta({ label: '用户名' }),
  password: afz.string({ type: 'withPasswordToggle' }).min(8).meta({ label: '密码' }),
  bio: afz.string({ type: 'textarea' }).default('一个简单的自我介绍').meta({ label: '简介' })
})

const state = reactive<Partial<z.output<typeof schema>>>({})
</script>

<template>
  <MAutoForm :schema="schema" :state="state">
    <template #field-label:username="{ label }">
      <UBadge icon="i-lucide-user" color="success" variant="subtle" size="xs">
        {{ label }}
      </UBadge>
    </template>

    <template #field-help:password>
      <UAlert
        color="warning"
        variant="subtle"
        icon="i-lucide-shield-check"
        description="field-help:password 只替换 password 字段的帮助说明，并保留字段默认控件"
        class="mt-2"
      />
    </template>

    <template #field-default:bio="{ setValue, value }">
      <UTextarea
        :model-value="value"
        :rows="5"
        placeholder="field-default:bio 完全替换默认 textarea"
        class="ring-2 ring-primary/30 rounded w-full"
        @update:model-value="setValue"
      />
    </template>
  </MAutoForm>
</template>
```

### Before / After Field

`field-before:{path}` and `field-after:{path}` append content before and after the default field content without taking over child field rendering and interaction.

```vue [AutoFormSlotsBeforeAfterExample.vue]
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  settings: afz.object({
    notifications: afz.boolean({ type: 'switch', controlProps: { label: '接收通知' } }).default(true).meta({ label: '通知' }),
    digest: afz.enum(['daily', 'weekly', 'monthly']).default('weekly').meta({ label: '摘要频率' }),
    notes: afz.string({ type: 'textarea' }).default('保留默认字段渲染，只在前后插入 slot 内容。').meta({ label: '备注' })
  }).default({
    notifications: true,
    digest: 'weekly',
    notes: '保留默认字段渲染，只在前后插入 slot 内容。'
  }).meta({ label: '通知设置', collapsible: { defaultOpen: true } })
})

const state = reactive<Partial<z.output<typeof schema>>>({})
</script>

<template>
  <MAutoForm :schema="schema" :state="state">
    <template #field-before:settings="{ path }">
      <UAlert
        color="neutral"
        variant="subtle"
        icon="i-lucide-settings"
        title="field-before 插入点"
        :description="`field-before:${path} 不接管子字段，只在默认内容前追加说明。`"
        class="mb-4"
      />
    </template>

    <template #field-after:settings="{ value }">
      <div class="mt-4 flex flex-wrap items-center gap-2">
        <UBadge :color="value?.notifications ? 'success' : 'neutral'" variant="subtle">
          {{ value?.notifications ? '通知开启' : '通知关闭' }}
        </UBadge>
        <UBadge color="info" variant="subtle">
          {{ value?.digest || '未选择摘要频率' }}
        </UBadge>
        <span class="text-xs text-muted">
          field-after 保留默认字段交互，只追加汇总状态。
        </span>
      </div>
    </template>
  </MAutoForm>
</template>
```

### Object Content

`field-content:{path}` completely replaces the rendering of an object field; use `setValue('key', value)` with a relative key to update child fields.

```vue [AutoFormSlotsObjectContentExample.vue]
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  profile: afz.object({
    displayName: afz.string().meta({ label: '显示名称' }),
    website: afz.url().meta({ label: '个人网站' }),
    bio: afz.string({ type: 'textarea' }).meta({ label: '简介' })
  }).default({
    displayName: 'Movk User',
    website: 'https://movk.dev',
    bio: '使用 field-content:profile 接管对象字段渲染。'
  }).meta({ label: '个人资料', collapsible: { defaultOpen: true } })
})

const state = reactive<Partial<z.output<typeof schema>>>({})
</script>

<template>
  <MAutoForm :schema="schema" :state="state">
    <template #field-content:profile="{ path, value, setValue }">
      <div class="space-y-4 rounded-md border border-default p-4">
        <div class="flex items-center justify-between">
          <div>
            <p class="font-medium">
              自定义个人资料
            </p>
            <p class="text-sm text-muted">
              每个控件都通过相对 key 更新 profile 子字段
            </p>
          </div>
          <UBadge color="info" variant="subtle">
            object
          </UBadge>
        </div>

        <UFormField label="显示名称" :name="`${path}.displayName`" required>
          <UInput
            :model-value="value?.displayName"
            icon="i-lucide-user"
            placeholder="显示名称"
            @update:model-value="setValue('displayName', $event)"
          />
        </UFormField>

        <UFormField label="个人网站">
          <UInput
            :model-value="value?.website"
            icon="i-lucide-link"
            placeholder="https://movk.dev"
            @update:model-value="setValue('website', $event)"
          />
        </UFormField>

        <UFormField label="简介">
          <UTextarea
            :model-value="value?.bio"
            :rows="4"
            placeholder="简介"
            @update:model-value="setValue('bio', $event)"
          />
        </UFormField>
      </div>
    </template>
  </MAutoForm>
</template>
```

### Array Content

`field-content:{path}` replaces the rendering of an array field; `setValue` can write the entire list or update a single item using an `[index].key` path.

```vue [AutoFormSlotsArrayContentExample.vue]
<script lang="ts" setup>
import type { z } from 'zod'

const { afz } = useAutoForm()

const schema = afz.object({
  todos: afz.array(
    afz.object({
      title: afz.string().min(1).meta({ label: '标题' }),
      priority: afz.enum(['low', 'medium', 'high']).default('medium').meta({ label: '优先级' }),
      done: afz.boolean().default(false).meta({ label: '完成' })
    })
  ).default([
    { title: '整理 slot 示例', priority: 'high', done: false },
    { title: '梳理 setValue 路径', priority: 'medium', done: false }
  ]).meta({ label: '待办列表', collapsible: { defaultOpen: true } })
})

const state = reactive<Partial<z.output<typeof schema>>>({})

const priorityItems = [
  { label: '低', value: 'low' },
  { label: '中', value: 'medium' },
  { label: '高', value: 'high' }
]

function createTodo() {
  return { title: '', priority: 'medium' as const, done: false }
}
</script>

<template>
  <MAutoForm :schema="schema" :state="state">
    <template #field-content:todos="{ value, setValue }">
      <div class="space-y-3">
        <div
          v-for="(todo, index) in value || []"
          :key="index"
          class="rounded-md border border-default p-4 space-y-3"
        >
          <div class="flex items-center justify-between gap-3">
            <UBadge color="neutral" variant="subtle">
              #{{ index + 1 }}
            </UBadge>
            <UButton
              icon="i-lucide-trash-2"
              color="error"
              variant="ghost"
              size="xs"
              @click="setValue((value || []).filter((_, current) => current !== index))"
            />
          </div>

          <UFormField label="标题">
            <UInput
              :model-value="todo?.title"
              placeholder="任务标题"
              @update:model-value="setValue(`[${index}].title`, $event)"
            />
          </UFormField>

          <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
            <UFormField label="优先级">
              <USelect
                :model-value="todo?.priority"
                :items="priorityItems"
                @update:model-value="setValue(`[${index}].priority`, $event)"
              />
            </UFormField>

            <UFormField label="完成状态">
              <USwitch
                :model-value="todo?.done"
                :label="todo?.done ? '已完成' : '进行中'"
                @update:model-value="setValue(`[${index}].done`, $event)"
              />
            </UFormField>
          </div>
        </div>

        <UButton
          icon="i-lucide-plus"
          color="info"
          variant="soft"
          size="sm"
          @click="setValue([...(value || []), createTodo()])"
        >
          添加任务
        </UButton>
      </div>
    </template>
  </MAutoForm>
</template>
```

## API

### Form-Level Slots

| Name | Description |
| --- | --- |
| `header` | Above the fields area, suitable for titles, descriptions or status hints |
| `footer` | Below the fields area, above the submit button |
| `submit` | Takes over the submit area, use with `:submit="false"` |

### Field Slots

| Slot | Parameter | Type | Description |
| --- | --- | --- | --- |
| `field-label` | `label` | `string` | Label text |
| `field-hint` | `hint` | `string` | Hint text |
| `field-description` | `description` | `string` | Description text |
| `field-help` | `help` | `string` | Help text |
| `field-error` | `error` | `string \| boolean` | Error message |
| `field-default` | `error` | `string \| boolean` | Takes over control rendering for primitive type fields (with field context) |

> [!TIP]
> 
> field-default
> 
>  is only for customizing the control of primitive type fields (string, number, boolean, etc.). For object and array fields, use 
> 
> field-content
> 
> .

### Content Slots

> [!WARNING]
> 
> Content slots only apply to 
> 
> object fields and array fields
> 
>  and have no effect on primitive type fields.

| Name | Description |
| --- | --- |
| `field-content:{path}` | Completely takes over the rendering of an object/array field |
| `field-before:{path}` | Inserts before the default content without taking over child fields |
| `field-after:{path}` | Inserts after the default content without taking over child fields |

### `setValue` Signature

`setValue` supports the following overload forms:

| Signature | Return | Description |
| --- | --- | --- |
| `setValue(value)` | `void` | Set the entire field value |
| `setValue(relativePath, value)` | `void` | Update a child field by relative path: use key for objects (auto-inferred), `[index].key` for arrays |

> [!NOTE]
> 
> ```vue
> // Set the current field value directly
> setValue(newValue)
> ```
> 
> ```vue
> <template #field-content:profile="{ value, setValue }">
>   <!-- Set the entire object -->
>   <UButton @click="setValue({ name: 'John', email: 'test@example.com' })">
>     Fill Defaults
>   </UButton>
> 
>   <!-- Set a child field using a relative path -->
>   <UInput
>     :model-value="value?.name"
>     @update:model-value="setValue('name', $event)"
>   />
> </template>
> ```
> 
> ```vue
> <template #field-content:tasks="{ value, setValue }">
>   <div v-for="(task, index) in value" :key="index">
>     <!-- Set an array element's property -->
>     <UInput
>       :model-value="task?.title"
>       @update:model-value="setValue(`[${index}].title`, $event)"
>     />
> 
>     <!-- Remove an array element -->
>     <UButton
>       @click="setValue(value.filter((_, i) => i !== index))"
>       icon="i-lucide-trash-2"
>     />
>   </div>
> 
>   <!-- Add a new element -->
>   <UButton
>     @click="setValue([...value, { title: '', completed: false }])"
>     icon="i-lucide-plus"
>   />
> </template>
> ```

### `AutoFormFieldContext`

Field context type, available in slots and reactive functions:

```ts
export interface AutoFormFieldContext<S = any, P extends string = string> {
  /** Form data */
  readonly state: S
  /** Field path */
  readonly path: P
  /** Field value (may be undefined) */
  readonly value: FieldValueType<S, P>
  /**
   * Callback function to set the field value
   * @description Supports multiple invocation styles:
   * 1. setValue(value) - set the entire value of the current field
   * 2. setValue(key, value) - set a child field value (object) or element property (array)
   * 3. setValue(path, value) - set a value at an arbitrary path (string fallback)
   * @example
   * // Object field profile: { name, email, bio }
   * setValue({ name: 'John', email: 'test@example.com' })  // set the entire object
   * setValue('name', 'John')  // key auto-inferred as 'name' | 'email' | 'bio'
   *
   * // Array field tasks: [{ title, priority, completed }]
   * setValue([...tasks, newTask])  // set the entire array
   * setValue('[0].title', 'New Title')  // use index path string
   *
   * // Or use property name directly when iterating (recommended)
   * v-for="(task, index) in tasks"
   * setValue(`[${index}].title`, value)  // dynamic index
   */
  setValue: {
    (value: FieldValueType<S, P>): void
    <K extends RelativePath<NonUndefinedFieldValue<S, P>>>(
      relativePath: K extends never ? string : K,
      value: any
    ): void
    (relativePath: string, value: any): void
  }
  /** Field error list */
  readonly errors: unknown[]
  /** Form submit loading state */
  readonly loading: boolean
  /** Field collapsed state */
  readonly open?: boolean
  /** Array element index */
  readonly count?: number
}
```


## Sitemap

See the full [sitemap](https://nuxt.mhaibaraai.cn/sitemap.md) for all pages.
