Layout

View source
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:

<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>
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:

此字段占据两列宽度

<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:

<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>
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:

此字段占据两列宽度
<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>
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:

<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>
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:

<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>
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.
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:

AttributeTypeDescription
componentComponent | stringLayout container component (default: 'div')
classstring | (context) => stringCSS class name, supports reactive function
propsobject | (context) => objectComponent attributes, supports reactive function
slotsRecord<string, () => VNode> | (context) => ...Component slot content
fieldsRecord<string, ZodType>Contained field definitions
fieldSlotstring | (context) => stringRender all fields into the specified slot
fieldSlotsRecord<string, string> | (context) => ...Assign different fields to different slots

AutoFormNestedCollapsible

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

AttributeTypeDefaultDescription
enabledbooleantrueWhether to enable collapsible functionality
defaultOpenbooleanWhether to be open by default
openbooleanControl expand/collapse state (controlled mode)
disabledbooleanDisable collapsible (always expanded)
unmountOnHidebooleantrueUnmount content when hidden
asstring'div'Rendered element type
classClassNameValueCSS class name
ui{ root?: ClassNameValue; content?: ClassNameValue; }UI style configuration
Copyright © 2025 - 2026 YiXuan - MIT License