---
title: "Layout"
description: "Customize field layout via the layout config, supporting grid, tabs, accordion containers and collapsible nested fields."
canonical_url: "https://nuxt.mhaibaraai.cn/en/docs/auto-form/layout"
---
# Layout

> Customize field layout via the layout config, supporting grid, tabs, accordion containers and collapsible nested fields.

## Examples

### Simple Container Layout

Use `afz.layout` to create a basic container layout, controlling styles via the `class` attribute:

```vue [AutoFormLayoutSimpleExample.vue]
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'

const { afz } = useAutoForm()
const toast = useToast()

const schema = afz.object({
  $layout: afz.layout({
    class: 'space-y-4',
    fields: {
      firstName: afz.string().meta({ label: '名字' }),
      lastName: afz.string().meta({ label: '姓氏' }),
      email: afz.email().meta({ label: '邮箱地址' })
    }
  })
})

type Schema = z.output<typeof schema>

const form = ref<Partial<Schema>>({})

async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({
    title: '提交成功',
    color: 'success',
    description: JSON.stringify(event.data, null, 2)
  })
}
</script>

<template>
  <MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
```

> [!NOTE]
> 
> It is conventional for layout field keys (e.g. 
> 
> $layout
> 
> ) to start with 
> 
> $
> 
> , but it is not required. You can use any field name as long as its value is 
> 
> afz.layout()
> 
> .

### Grid Layout

Use CSS Grid to create multi-column layouts, controlling column spans via field-level `class`:

```vue [AutoFormLayoutGridExample.vue]
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'

const { afz } = useAutoForm()
const toast = useToast()

const schema = afz.object({
  $grid: afz.layout({
    class: 'grid grid-cols-2 gap-4',
    fields: {
      firstName: afz.string().meta({ label: '名字' }),
      lastName: afz.string().meta({ label: '姓氏' }),
      email: afz.email().meta({
        class: 'col-span-2',
        label: '邮箱地址',
        description: '此字段占据两列宽度'
      }),
      phone: afz.string().meta({ label: '电话' }),
      age: afz.number().int().min(0).meta({ label: '年龄' })
    }
  })
})

type Schema = z.output<typeof schema>

const form = ref<Partial<Schema>>({})

async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({
    title: '提交成功',
    color: 'success',
    description: JSON.stringify(event.data, null, 2)
  })
}
</script>

<template>
  <MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
```

### Responsive Multi-Column Layout

Use Tailwind's responsive prefixes to create layouts that adapt to different screen sizes:

```vue [AutoFormLayoutResponsiveExample.vue]
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'

const { afz } = useAutoForm()
const toast = useToast()

const schema = afz.object({
  $responsive: afz.layout({
    // 移动端 1 列，平板 2 列，桌面端 3 列
    class: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4',
    fields: {
      title: afz.string().meta({
        class: 'col-span-full', // 所有屏幕尺寸都占满整行
        label: '标题'
      }),
      firstName: afz.string().meta({ label: '名字' }),
      lastName: afz.string().meta({ label: '姓氏' }),
      email: afz.email().meta({ label: '邮箱' }),
      phone: afz.string().meta({ label: '电话' }),
      age: afz.number().int().min(0).meta({ label: '年龄' }),
      description: afz.string({ type: 'textarea' }).meta({
        class: 'col-span-full',
        label: '描述'
      })
    }
  })
})

type Schema = z.output<typeof schema>

const form = ref<Partial<Schema>>({})

async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({
    title: '提交成功',
    color: 'success',
    description: JSON.stringify(event.data, null, 2)
  })
}
</script>

<template>
  <MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
```

> [!TIP]
> 
> Use Tailwind's responsive prefixes (
> 
> md:
> 
> , 
> 
> lg:
> 
> ) to easily achieve cross-device adaptive layouts. Fields can span the full row with 
> 
> col-span-full
> 
> .

### Nested Layout

Layout fields can be nested infinitely to build complex hierarchical structures:

```vue [AutoFormLayoutNestedExample.vue]
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'

const { afz } = useAutoForm()
const toast = useToast()

const schema = afz.object({
  $personalInfo: afz.layout({
    class: 'grid grid-cols-2 gap-4',
    fields: {
      firstName: afz.string().meta({ label: '名字' }),
      lastName: afz.string().meta({ label: '姓氏' })
    }
  }),

  $contactInfo: afz.layout({
    class: 'space-y-4 mt-10',
    fields: {
      email: afz.email().meta({ label: '邮箱地址' }),
      $phoneLayout: afz.layout({
        class: 'grid grid-cols-3 gap-2',
        fields: {
          countryCode: afz.string().default('+86').meta({ label: '国家代码' }),
          phoneNumber: afz.string().meta({
            class: 'col-span-2',
            label: '电话号码',
            hint: '此字段占据两列宽度'
          })
        }
      })
    }
  })
})

type Schema = z.output<typeof schema>

const form = ref<Partial<Schema>>({})

async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({
    title: '提交成功',
    color: 'success',
    description: JSON.stringify(event.data, null, 2)
  })
}
</script>

<template>
  <MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
```

> [!WARNING]
> 
> There is no limit to nested layout depth, but deep nesting can hurt readability. Keeping it to 2-3 levels is recommended.

### Accordion Layout

Use Nuxt UI's `UAccordion` component to organize fields in collapsible panels:

```vue [AutoFormLayoutAccordionExample.vue]
<script setup lang="ts">
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'
import { UAccordion } from '#components'

const { afz } = useAutoForm()
const toast = useToast()

const schema = afz.object({
  $accordion: afz.layout({
    component: UAccordion,
    props: {
      type: 'multiple',
      ui: {
        content: 'space-y-4'
      },
      items: [
        { label: '基本信息', icon: 'i-lucide-user', slot: 'item-0' },
        { label: '详细信息', icon: 'i-lucide-square-pen', slot: 'item-1' }
      ]
    },
    fieldSlots: {
      name: 'item-0',
      email: 'item-0',
      bio: 'item-1'
    },
    fields: {
      name: afz.string().meta({ label: '姓名' }),
      email: afz.email().meta({ label: '邮箱' }),
      bio: afz.string({ type: 'textarea' }).meta({ label: '个人简介' }).optional()
    }
  })
})

type Schema = z.output<typeof schema>

const form = ref<Partial<Schema>>({})

async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({
    title: '提交成功',
    color: 'success',
    description: JSON.stringify(event.data, null, 2)
  })
}
</script>

<template>
  <MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
```

> [!TIP]
> 
> fieldSlots
> 
>  is used to assign different fields to different slots of the layout component. Slot names must match the 
> 
> slot
> 
>  defined in 
> 
> items
> 
> .

### Tabs Layout

Use the `UTabs` component to create a tabbed form with dynamic responsive configuration:

```vue [AutoFormLayoutTabsExample.vue]
<script setup lang="ts">
import { UTabs } from '#components'
import type { FormSubmitEvent } from '@nuxt/ui'
import type { z } from 'zod'
import type { AutoFormFieldContext } from '#movk/types'

const { afz } = useAutoForm()
const toast = useToast()

const schema = afz.object({
  userType: afz.enum(['personal', 'company'])
    .default('personal')
    .meta({ label: '用户类型' }),

  $tabs: afz.layout({
    component: UTabs,
    props: ({ state }) => {
      const isCompany = state?.userType === 'company'
      return {
        ui: { content: 'space-y-4' },
        items: isCompany
          ? [
              { label: '公司信息', icon: 'i-lucide-building', slot: 'tab-0' },
              { label: '联系方式', icon: 'i-lucide-phone', slot: 'tab-1' }
            ]
          : [
              { label: '个人信息', icon: 'i-lucide-user', slot: 'tab-0' },
              { label: '联系方式', icon: 'i-lucide-phone', slot: 'tab-1' }
            ]
      }
    },
    fieldSlots: ({ state }) => {
      if (state?.userType === 'company') {
        return {
          companyName: 'tab-0',
          registrationNumber: 'tab-0',
          phone: 'tab-1',
          email: 'tab-1',
          address: 'tab-1'
        }
      }
      return {
        username: 'tab-0',
        age: 'tab-0',
        phone: 'tab-1',
        email: 'tab-1',
        address: 'tab-1'
      }
    },
    fields: {
      username: afz.string().meta({
        label: '姓名',
        if: ({ state }: AutoFormFieldContext) => state?.userType === 'personal'
      }).optional(),
      age: afz.number().int().min(0).meta({
        label: '年龄',
        if: ({ state }: AutoFormFieldContext) => state?.userType === 'personal'
      }).optional(),
      companyName: afz.string().meta({
        label: '公司名称',
        if: ({ state }: AutoFormFieldContext) => state?.userType === 'company'
      }).optional(),
      registrationNumber: afz.string().meta({
        label: '注册号',
        if: ({ state }: AutoFormFieldContext) => state?.userType === 'company'
      }).optional(),
      phone: afz.string().meta({ label: '联系电话' }),
      email: afz.email().meta({ label: '邮箱地址' }),
      address: afz.string().meta({ label: '地址' })
    }
  })
})

type Schema = z.output<typeof schema>

const form = ref<Partial<Schema>>({})

async function onSubmit(event: FormSubmitEvent<Schema>) {
  toast.add({
    title: '提交成功',
    color: 'success',
    description: JSON.stringify(event.data, null, 2)
  })
}
</script>

<template>
  <MAutoForm :schema="schema" :state="form" @submit="onSubmit" />
</template>
```

> [!TIP]
> 
> The tabs layout demonstrates how to dynamically adjust the layout structure based on form state. When switching user types, tab titles and field distribution update automatically.

> [!WARNING]
> 
> When using conditional rendering (
> 
> if
> 
> ), it is recommended to mark fields as 
> 
> .optional()
> 
>  to avoid validation errors.

## API

### `AutoFormLayoutConfig`

`afz.layout` accepts a configuration object with the following attributes:

| Attribute | Type | Description |
| --- | --- | --- |
| `component` | `Component \| string` | Layout container component (default: `'div'`) |
| `class` | `string \| (context) => string` | CSS class name, supports reactive function |
| `props` | `object \| (context) => object` | Component attributes, supports reactive function |
| `slots` | `Record<string, () => VNode> \| (context) => ...` | Component slot content |
| `fields` | `Record<string, ZodType>` | Contained field definitions |
| `fieldSlot` | `string \| (context) => string` | Render **all** fields into the specified slot |
| `fieldSlots` | `Record<string, string> \| (context) => ...` | Assign **different** fields to different slots |

### `AutoFormNestedCollapsible`

The `collapsible` attribute accepts a configuration object with the following options:

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | Whether to enable collapsible functionality |
| `defaultOpen` | `boolean` |  | Whether to be open by default |
| `open` | `boolean` |  | Control expand/collapse state (controlled mode) |
| `disabled` | `boolean` |  | Disable collapsible (always expanded) |
| `unmountOnHide` | `boolean` | `true` | Unmount content when hidden |
| `as` | `string` | `'div'` | Rendered element type |
| `class` | `ClassNameValue` |  | CSS class name |
| `ui` | `{ root?: ClassNameValue; content?: ClassNameValue; }` |  | UI style configuration |


## Sitemap

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