FormView Layout Types
Guide to all 6 FormView layout types available in HotCRM — simple, tabbed, wizard, modal, drawer, and split.
FormView Layout Types
FormViews define how data entry forms are rendered for each business object. Every *.form.ts file exports a FormView validated against FormViewSchema from @objectstack/spec/ui. This guide covers all six layout types with real examples from HotCRM packages.
File convention:
packages/{package}/src/{object}.form.ts— or{object}_{variant}.form.tsfor alternate layouts.
Schema Overview
Every FormView follows the same top-level structure:
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';
export const MyForm = {
type: 'simple' | 'tabbed' | 'wizard' | 'modal' | 'drawer' | 'split',
data: {
provider: 'object',
object: 'my_object' // snake_case object name
},
sections: [ /* ... */ ]
} satisfies FormView;
FormViewSchema.parse(MyForm);
export default MyForm;Simple
A standard single-page form with one or more sections stacked vertically. This is the default layout and the most commonly used.
When to use: Full create/edit forms where all fields are visible on one page.
// packages/crm/src/lead.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';
export const LeadForm = {
type: 'simple' as const,
data: {
provider: 'object' as const,
object: 'lead'
},
sections: [
{
label: 'Lead Information',
columns: '2' as const,
fields: [
{ field: 'first_name', required: true, placeholder: 'First name' },
{ field: 'last_name', required: true, placeholder: 'Last name' },
{ field: 'company', required: true, placeholder: 'Company or organization' },
{ field: 'title', placeholder: 'Job title' },
{ field: 'email', required: true, placeholder: 'email@example.com' },
{ field: 'phone', placeholder: '+1 (555) 000-0000' },
{ field: 'mobile_phone' },
{ field: 'website', placeholder: 'https://', visibleOn: "company != ''" },
{ field: 'industry', dependsOn: 'company' },
{ field: 'lead_source' },
{ field: 'description', colSpan: 2, widget: 'richtext' }
]
},
{
label: 'Address',
columns: '2' as const,
collapsible: true,
collapsed: true,
fields: [
{ field: 'street', colSpan: 2 },
{ field: 'city' },
{ field: 'state', dependsOn: 'country' },
{ field: 'postal_code' },
{ field: 'country' }
]
}
]
} satisfies FormView;
FormViewSchema.parse(LeadForm);
export default LeadForm;Tabbed
Sections are rendered as horizontal tabs. Each section becomes its own tab pane, keeping long forms organized without vertical scrolling.
When to use: Records with many fields that fall into distinct categories (e.g., details, line items, payment terms).
// packages/finance/src/invoice.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';
export const InvoiceForm = {
type: 'tabbed' as const,
data: {
provider: 'object' as const,
object: 'invoice'
},
sections: [
{
label: 'Invoice Details',
columns: '2' as const,
fields: [
{ field: 'invoice_number', readonly: true, helpText: 'Auto-generated' },
{ field: 'account_id', label: 'Account', required: true },
{ field: 'contact_id', label: 'Contact' },
{ field: 'invoice_date', required: true },
{ field: 'due_date', required: true },
{ field: 'status' },
{ field: 'currency' }
]
},
{
label: 'Line Items',
columns: '1' as const,
fields: [
{ field: 'description', widget: 'richtext' },
{ field: 'subtotal', readonly: true },
{ field: 'tax_amount', readonly: true },
{ field: 'total_amount', readonly: true, helpText: 'Calculated from line items' }
]
},
{
label: 'Payment Terms',
columns: '2' as const,
fields: [
{ field: 'payment_terms' },
{ field: 'payment_method' },
{ field: 'billing_street' },
{ field: 'billing_city' },
{ field: 'billing_state' },
{ field: 'billing_postal_code' },
{ field: 'billing_country' }
]
}
]
} satisfies FormView;
FormViewSchema.parse(InvoiceForm);
export default InvoiceForm;Wizard
A multi-step form where each section is a sequential step. Users progress through steps with Next/Back navigation. Ideal for guided data entry workflows.
When to use: Onboarding flows, multi-step processes, or forms where later steps depend on earlier input.
// packages/hr/src/employee_onboarding.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';
export const EmployeeOnboardingForm = {
type: 'wizard' as const,
data: {
provider: 'object' as const,
object: 'employee'
},
sections: [
{
label: 'Personal Information',
columns: '2' as const,
fields: [
{ field: 'first_name', required: true },
{ field: 'last_name', required: true },
{ field: 'email', required: true, placeholder: 'work@company.com' },
{ field: 'personal_email' },
{ field: 'phone' },
{ field: 'date_of_birth' },
{ field: 'gender' }
]
},
{
label: 'Role & Department',
columns: '2' as const,
fields: [
{ field: 'department_id', label: 'Department', required: true },
{ field: 'position_id', label: 'Position', required: true },
{ field: 'manager_id', label: 'Manager' },
{ field: 'hire_date', required: true },
{ field: 'employment_type' },
{ field: 'work_location' }
]
},
{
label: 'IT Setup',
columns: '2' as const,
fields: [
{ field: 'employee_number', readonly: true, helpText: 'Auto-generated after provisioning' },
{ field: 'work_location' }
]
},
{
label: 'Review & Confirm',
columns: '1' as const,
fields: [
{ field: 'first_name', readonly: true },
{ field: 'last_name', readonly: true },
{ field: 'email', readonly: true },
{ field: 'department_id', readonly: true, label: 'Department' },
{ field: 'position_id', readonly: true, label: 'Position' },
{ field: 'hire_date', readonly: true }
]
}
]
} satisfies FormView;
FormViewSchema.parse(EmployeeOnboardingForm);
export default EmployeeOnboardingForm;Modal
A compact overlay dialog for quick record creation. Only essential fields are shown so users can create records without leaving their current context.
When to use: Quick-create actions, inline record creation from related lists, or any form that should not navigate away from the current page.
// packages/crm/src/lead_quick_create.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';
export const LeadQuickCreateForm = {
type: 'modal' as const,
data: {
provider: 'object' as const,
object: 'lead'
},
sections: [
{
label: 'Lead Information',
columns: '2' as const,
fields: [
{ field: 'first_name', required: true },
{ field: 'last_name', required: true },
{ field: 'company' },
{ field: 'email', required: true, placeholder: 'name@company.com' },
{ field: 'phone', placeholder: '+1 (555) 000-0000' },
{ field: 'lead_source' }
]
}
]
} satisfies FormView;
FormViewSchema.parse(LeadQuickCreateForm);
export default LeadQuickCreateForm;Drawer
A slide-in side panel for quick record creation. Similar to modal but anchored to the edge of the viewport, keeping the underlying page partially visible.
When to use: Quick-create from a list view or record page where you want context from the parent page to remain visible.
// packages/crm/src/opportunity_quick_create.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';
export const OpportunityQuickCreateForm = {
type: 'drawer' as const,
data: {
provider: 'object' as const,
object: 'opportunity'
},
sections: [
{
label: 'Deal Information',
columns: '2' as const,
fields: [
{ field: 'name', required: true, placeholder: 'Enter deal name' },
{ field: 'account_id', label: 'Account' },
{ field: 'amount' },
{ field: 'close_date', required: true },
{ field: 'stage', required: true }
]
}
]
} satisfies FormView;
FormViewSchema.parse(OpportunityQuickCreateForm);
export default OpportunityQuickCreateForm;Split
A side-by-side layout where sections are displayed in two equal panels. Each section occupies one panel, making it easy to compare or group related information.
When to use: Records that have two natural groupings (e.g., account info vs. address, billing vs. shipping).
// packages/crm/src/account_split.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';
export const AccountSplitForm = {
type: 'split' as const,
data: {
provider: 'object' as const,
object: 'account'
},
sections: [
{
label: 'Account Information',
columns: '1' as const,
fields: [
{ field: 'name', required: true },
{ field: 'type' },
{ field: 'industry' },
{ field: 'phone' },
{ field: 'website' },
{ field: 'annual_revenue' },
{ field: 'ownership' }
]
},
{
label: 'Billing & Shipping',
columns: '1' as const,
fields: [
{ field: 'billing_street' },
{ field: 'billing_city' },
{ field: 'billing_state' },
{ field: 'billing_postal_code' },
{ field: 'billing_country' },
{ field: 'shipping_street' },
{ field: 'shipping_city' },
{ field: 'shipping_state' },
{ field: 'shipping_postal_code' },
{ field: 'shipping_country' }
]
}
]
} satisfies FormView;
FormViewSchema.parse(AccountSplitForm);
export default AccountSplitForm;Layout Type Summary
| Type | Sections Rendered As | Best For |
|---|---|---|
simple | Stacked vertically | Standard create/edit forms |
tabbed | Horizontal tab panes | Multi-category records |
wizard | Sequential steps | Guided workflows, onboarding |
modal | Overlay dialog | Quick-create without navigation |
drawer | Slide-in side panel | Quick-create with context |
split | Side-by-side panels | Comparing or grouping data |
Field-Level Features
Every field entry in a section supports the following properties:
| Property | Type | Description |
|---|---|---|
field | string | Required. The API name of the field on the object. |
label | string | Override the default field label. |
required | boolean | Mark the field as required for form submission. |
readonly | boolean | Render the field as read-only (e.g., auto-generated values). |
hidden | boolean | Hide the field from the form while still including it in the data model. |
helpText | string | Inline help text displayed below the field (e.g., 'Auto-generated'). |
placeholder | string | Placeholder text shown inside the input when empty. |
widget | string | Override the default widget (e.g., 'richtext' for a rich text editor). |
visibleOn | string | Expression that controls field visibility (e.g., "company != ''"). |
dependsOn | string | Field that must have a value before this field becomes active. |
colSpan | number | Number of grid columns this field spans (e.g., 2 for full-width). |
Example: Field Features in Action
fields: [
{ field: 'invoice_number', readonly: true, helpText: 'Auto-generated' },
{ field: 'website', placeholder: 'https://', visibleOn: "company != ''" },
{ field: 'state', dependsOn: 'country' },
{ field: 'description', colSpan: 2, widget: 'richtext' },
{ field: 'email', required: true, placeholder: 'email@example.com' }
]Section Features
Each section in the sections array supports:
| Property | Type | Description |
|---|---|---|
label | string | Required. The section heading (also used as the tab/step title). |
columns | '1' | '2' | Number of columns for the field grid layout. |
collapsible | boolean | Allow the section to be collapsed by the user. |
collapsed | boolean | Start the section in a collapsed state (requires collapsible: true). |
fields | array | Array of field definitions for this section. |
Example: Collapsible Section
{
label: 'Address',
columns: '2' as const,
collapsible: true,
collapsed: true,
fields: [
{ field: 'street', colSpan: 2 },
{ field: 'city' },
{ field: 'state', dependsOn: 'country' },
{ field: 'postal_code' },
{ field: 'country' }
]
}Quick Reference
| What you want | How to do it |
|---|---|
| Full-width field | colSpan: 2 in a 2-column section |
| Conditional visibility | visibleOn: "field_name != ''" |
| Dependent dropdown | dependsOn: 'parent_field' |
| Rich text editor | widget: 'richtext' |
| Auto-generated field | readonly: true + helpText: 'Auto-generated' |
| Collapsed by default | collapsible: true + collapsed: true on the section |
| Quick-create dialog | type: 'modal' with minimal fields |
| Side panel form | type: 'drawer' |
| Step-by-step flow | type: 'wizard' with one section per step |
MCP Integration Guide
How to build AI-powered CRM features using the Model Context Protocol (MCP) — tools, resources, prompts, orchestrations, and predictive models.
Page Components Reference
Guide to all component types available in HotCRM page layouts — record, page, and AI components with property reference.