Skip to content

Form Layout Guide

@enlolab/forms provides a semantic, UX-first layout system that allows you to create responsive form layouts without thinking about technical details like grid columns or breakpoints. The system adapts automatically to any container width, making it perfect for forms that live in sidebars, modals, or full-width pages.

The layout system offers two modes:

The Simple mode is perfect for 90% of use cases. It automatically adapts to the container width using CSS Grid’s auto-fit feature. You only need to think in terms of:

  • Density: How spaced out should fields be?
  • Field width: Should this field be full width, half, or third?
layout: {
mode: "auto",
auto: {
density: "normal", // "relaxed" | "normal" | "compact"
minFieldWidth: 240, // Optional: minimum field width in pixels
maxColumns: 3, // Optional: maximum number of columns
}
}

How it works:

  • The form automatically calculates how many columns fit based on the container width
  • Fields with width: "full" always take the full width
  • Other fields automatically arrange themselves side by side when there’s space
  • Works perfectly in containers of any width (30%, 50%, 100%, etc.)

The Advanced mode gives you full control over columns and breakpoints. Use this when you need precise control over the layout.

layout: {
mode: "grid",
grid: {
columns: {
mobile: 1, // 1 column on mobile
tablet: 2, // 2 columns on tablet
desktop: 4, // 4 columns on desktop
},
gap: {
mobile: "sm", // Small gap on mobile
desktop: "md", // Medium gap on desktop
},
}
}

Density controls the spacing between fields and affects the overall feel of the form:

  • relaxed: More space between fields (gap-6) - better for forms with few fields
  • normal: Standard spacing (gap-4) - good for most forms
  • compact: Tighter spacing (gap-2) - good for dense forms with many fields
layout: {
mode: "auto",
auto: {
density: "relaxed", // or "normal" | "compact"
}
}

Instead of thinking about colSpan numbers, you think in semantic terms:

  • "auto": Let the system decide (default)
  • "full": Field takes full width (good for textareas, date ranges, etc.)
  • "half": Field takes half width (good for pairs like first/last name)
  • "third": Field takes one third width (good for groups of three)
fields: {
fullName: {
type: "text",
label: "Full Name",
layout: {
width: {
mobile: "full", // Full width on mobile
desktop: "half", // Half width on desktop
}
}
},
bio: {
type: "textarea",
label: "Biography",
layout: {
width: {
mobile: "full",
desktop: "full", // Always full width
}
}
}
}

The system uses semantic breakpoints instead of technical Tailwind breakpoints:

  • mobile: Mobile devices (default, no prefix)
  • tablet: Tablets (maps to md: in Tailwind)
  • desktop: Desktop (maps to lg: in Tailwind)

This makes it easier to think about “mobile vs desktop” rather than “sm vs lg”.

const contactForm = FormFactory({
formId: "contact-form",
layout: {
mode: "auto",
auto: {
density: "normal",
minFieldWidth: 260,
maxColumns: 3,
},
},
fields: {
firstName: {
type: "text",
label: "First Name",
layout: {
width: {
mobile: "full",
desktop: "half",
},
},
schema: z.string().min(1),
},
lastName: {
type: "text",
label: "Last Name",
layout: {
width: {
mobile: "full",
desktop: "half",
},
},
schema: z.string().min(1),
},
email: {
type: "email",
label: "Email",
layout: {
width: {
mobile: "full",
desktop: "full",
},
},
schema: z.string().email(),
},
message: {
type: "textarea",
label: "Message",
layout: {
width: {
mobile: "full",
desktop: "full",
},
},
schema: z.string().min(10),
},
},
});
const advancedForm = FormFactory({
formId: "advanced-form",
layout: {
mode: "grid",
grid: {
columns: {
mobile: 1,
tablet: 2,
desktop: 4,
},
gap: {
mobile: "sm",
tablet: "md",
desktop: "lg",
},
},
},
fields: {
header: {
type: "html",
html: "<h2>Form Header</h2>",
layout: {
width: {
desktop: "full", // Spans all 4 columns
},
},
},
field1: {
type: "text",
label: "Field 1",
layout: {
width: {
desktop: "half", // Spans 2 of 4 columns
},
},
schema: z.string(),
},
field2: {
type: "text",
label: "Field 2",
layout: {
width: {
desktop: "half", // Spans 2 of 4 columns
},
},
schema: z.string(),
},
},
});

You can control field visibility per breakpoint:

fields: {
mobileOnlyField: {
type: "text",
label: "Mobile Only",
layout: {
visibility: {
desktop: "hidden", // Hidden on desktop
}
},
schema: z.string(),
},
desktopOnlyField: {
type: "text",
label: "Desktop Only",
layout: {
visibility: {
mobile: "hidden", // Hidden on mobile
}
},
schema: z.string(),
},
}

Control the visual order of fields:

fields: {
firstField: {
type: "text",
label: "First",
layout: {
order: 1,
},
schema: z.string(),
},
secondField: {
type: "text",
label: "Second",
layout: {
order: 2,
},
schema: z.string(),
},
}
  1. Use Simple Mode by Default: Start with mode: "auto" - it handles 90% of cases perfectly
  2. Think Semantically: Use "full", "half", "third" instead of column numbers
  3. Mobile First: Always specify mobile width, desktop is optional
  4. Density Matters: Choose density based on form complexity
  5. Full Width for Long Fields: Use width: "full" for textareas, date ranges, sliders
  6. Test in Different Containers: The auto mode adapts to any container width
  • ✅ You want the form to adapt to any container width
  • ✅ You don’t need precise control over columns
  • ✅ You’re building a standard form (contact, registration, etc.)
  • ✅ You want the simplest possible configuration
  • ✅ You need exact control over columns per breakpoint
  • ✅ You’re building a complex dashboard or admin form
  • ✅ You need custom breakpoint mappings
  • ✅ You’re a developer who wants full control

One of the key benefits of Simple mode is automatic adaptation:

  • 30% width container: Form will typically show 1 column
  • 50% width container: Form will show 1-2 columns
  • 100% width container: Form will show 2-3 columns (based on maxColumns)

The system uses minFieldWidth to determine how many columns fit, so it works perfectly in any context.